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.
- package/CONTRIBUTING.md +291 -292
- package/INSTRUCTIONS.md +327 -327
- package/LICENSE +202 -202
- package/README.md +231 -227
- package/dist/cli/logger.d.ts +26 -0
- package/dist/cli/logger.js +36 -0
- package/dist/cli/logo.js +5 -5
- package/dist/generators/components/connector-generator/generator.d.ts +12 -0
- package/dist/generators/components/connector-generator/generator.js +116 -0
- package/dist/generators/components/connector-generator/templates.d.ts +18 -0
- package/dist/generators/components/connector-generator/templates.js +222 -0
- package/dist/generators/components/connector-generator/types.d.ts +32 -0
- package/dist/generators/components/connector-generator/types.js +7 -0
- package/dist/generators/components/schema-analyzer/index.d.ts +17 -0
- package/dist/generators/components/schema-analyzer/index.js +20 -0
- package/dist/generators/components/schema-analyzer/intent-detector.d.ts +17 -0
- package/dist/generators/components/schema-analyzer/intent-detector.js +143 -0
- package/dist/generators/components/schema-analyzer/openapi-reader.d.ts +11 -0
- package/dist/generators/components/schema-analyzer/openapi-reader.js +76 -0
- package/dist/generators/components/schema-analyzer/resource-grouper.d.ts +6 -0
- package/dist/generators/components/schema-analyzer/resource-grouper.js +132 -0
- package/dist/generators/components/schema-analyzer/schema-field-mapper.d.ts +35 -0
- package/dist/generators/components/schema-analyzer/schema-field-mapper.js +220 -0
- package/dist/generators/components/schema-analyzer/types.d.ts +156 -0
- package/dist/generators/components/schema-analyzer/types.js +7 -0
- package/dist/generators/nuxt-server/generator.d.ts +2 -1
- package/dist/generators/nuxt-server/generator.js +21 -21
- package/dist/generators/shared/runtime/apiHelpers.d.ts +81 -41
- package/dist/generators/shared/runtime/apiHelpers.js +97 -104
- package/dist/generators/shared/runtime/pagination.d.ts +168 -0
- package/dist/generators/shared/runtime/pagination.js +179 -0
- package/dist/generators/shared/runtime/useDeleteConnector.d.ts +16 -0
- package/dist/generators/shared/runtime/useDeleteConnector.js +93 -0
- package/dist/generators/shared/runtime/useDetailConnector.d.ts +14 -0
- package/dist/generators/shared/runtime/useDetailConnector.js +50 -0
- package/dist/generators/shared/runtime/useFormConnector.d.ts +19 -0
- package/dist/generators/shared/runtime/useFormConnector.js +113 -0
- package/dist/generators/shared/runtime/useListConnector.d.ts +25 -0
- package/dist/generators/shared/runtime/useListConnector.js +125 -0
- package/dist/generators/shared/runtime/zod-error-merger.d.ts +23 -0
- package/dist/generators/shared/runtime/zod-error-merger.js +106 -0
- package/dist/generators/shared/templates/api-callbacks-plugin.js +54 -11
- package/dist/generators/shared/templates/api-pagination-plugin.d.ts +51 -0
- package/dist/generators/shared/templates/api-pagination-plugin.js +152 -0
- package/dist/generators/use-async-data/generator.d.ts +2 -1
- package/dist/generators/use-async-data/generator.js +14 -14
- package/dist/generators/use-async-data/runtime/useApiAsyncData.js +114 -13
- package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +88 -10
- package/dist/generators/use-async-data/templates.js +17 -17
- package/dist/generators/use-fetch/generator.d.ts +2 -1
- package/dist/generators/use-fetch/generator.js +12 -12
- package/dist/generators/use-fetch/runtime/useApiRequest.js +149 -40
- package/dist/generators/use-fetch/templates.js +14 -14
- package/dist/index.js +25 -0
- package/dist/module/index.d.ts +4 -0
- package/dist/module/index.js +93 -0
- package/dist/module/types.d.ts +27 -0
- package/dist/module/types.js +1 -0
- package/docs/API-REFERENCE.md +886 -887
- package/docs/generated-components.md +615 -0
- package/docs/headless-composables-ui.md +569 -0
- package/eslint.config.js +13 -0
- package/package.json +29 -2
- package/src/cli/config.ts +140 -140
- package/src/cli/logger.ts +124 -66
- package/src/cli/logo.ts +25 -25
- package/src/cli/types.ts +50 -50
- package/src/generators/components/connector-generator/generator.ts +138 -0
- package/src/generators/components/connector-generator/templates.ts +254 -0
- package/src/generators/components/connector-generator/types.ts +34 -0
- package/src/generators/components/schema-analyzer/index.ts +44 -0
- package/src/generators/components/schema-analyzer/intent-detector.ts +187 -0
- package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -0
- package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -0
- package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -0
- package/src/generators/components/schema-analyzer/types.ts +177 -0
- package/src/generators/nuxt-server/generator.ts +272 -270
- package/src/generators/shared/runtime/apiHelpers.ts +535 -507
- package/src/generators/shared/runtime/pagination.ts +323 -0
- package/src/generators/shared/runtime/useDeleteConnector.ts +109 -0
- package/src/generators/shared/runtime/useDetailConnector.ts +64 -0
- package/src/generators/shared/runtime/useFormConnector.ts +139 -0
- package/src/generators/shared/runtime/useListConnector.ts +148 -0
- package/src/generators/shared/runtime/zod-error-merger.ts +119 -0
- package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -352
- package/src/generators/shared/templates/api-pagination-plugin.ts +158 -0
- package/src/generators/use-async-data/generator.ts +205 -204
- package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -229
- package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -245
- package/src/generators/use-async-data/templates.ts +257 -257
- package/src/generators/use-fetch/generator.ts +170 -169
- package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -234
- package/src/generators/use-fetch/templates.ts +214 -214
- package/src/index.ts +303 -265
- package/src/module/index.ts +133 -0
- package/src/module/types.ts +31 -0
- package/src/generators/tanstack-query/generator.ts +0 -11
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { load as loadYaml } from 'js-yaml';
|
|
4
|
+
/**
|
|
5
|
+
* Read an OpenAPI spec from a YAML or JSON file.
|
|
6
|
+
* Returns the parsed spec with all $ref values resolved inline.
|
|
7
|
+
*/
|
|
8
|
+
export function readOpenApiSpec(filePath) {
|
|
9
|
+
const absPath = path.resolve(filePath);
|
|
10
|
+
const content = readFileSync(absPath, 'utf-8');
|
|
11
|
+
const raw = filePath.endsWith('.json')
|
|
12
|
+
? JSON.parse(content)
|
|
13
|
+
: loadYaml(content);
|
|
14
|
+
if (!raw || typeof raw !== 'object' || !raw.openapi || !raw.paths) {
|
|
15
|
+
throw new Error(`Invalid OpenAPI spec: ${absPath}`);
|
|
16
|
+
}
|
|
17
|
+
return resolveRefs(raw, raw);
|
|
18
|
+
}
|
|
19
|
+
// ─── $ref resolver ────────────────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Recursively walk the document and replace every { $ref: '#/...' } with the
|
|
22
|
+
* referenced value (deep-cloned to avoid circular references).
|
|
23
|
+
* Only local JSON Pointer refs (#/...) are supported.
|
|
24
|
+
*/
|
|
25
|
+
function resolveRefs(node, root, visited = new Set()) {
|
|
26
|
+
if (node === null || typeof node !== 'object') {
|
|
27
|
+
return node;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(node)) {
|
|
30
|
+
return node.map((item) => resolveRefs(item, root, visited));
|
|
31
|
+
}
|
|
32
|
+
const obj = node;
|
|
33
|
+
if (typeof obj['$ref'] === 'string') {
|
|
34
|
+
const ref = obj['$ref'];
|
|
35
|
+
if (visited.has(ref)) {
|
|
36
|
+
// Circular ref protection — return empty object rather than infinite loop
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
const resolved = resolvePointer(root, ref);
|
|
40
|
+
const newVisited = new Set(visited);
|
|
41
|
+
newVisited.add(ref);
|
|
42
|
+
return resolveRefs(resolved, root, newVisited);
|
|
43
|
+
}
|
|
44
|
+
const result = {};
|
|
45
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
46
|
+
result[key] = resolveRefs(value, root, visited);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a JSON Pointer like '#/components/schemas/Pet' against the root doc.
|
|
52
|
+
*/
|
|
53
|
+
function resolvePointer(root, ref) {
|
|
54
|
+
if (!ref.startsWith('#/')) {
|
|
55
|
+
throw new Error(`Only local $ref values are supported (got: ${ref})`);
|
|
56
|
+
}
|
|
57
|
+
const parts = ref
|
|
58
|
+
.slice(2)
|
|
59
|
+
.split('/')
|
|
60
|
+
.map((p) => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
61
|
+
let current = root;
|
|
62
|
+
for (const part of parts) {
|
|
63
|
+
if (current === null || typeof current !== 'object' || !(part in current)) {
|
|
64
|
+
throw new Error(`Cannot resolve $ref: ${ref}`);
|
|
65
|
+
}
|
|
66
|
+
current = current[part];
|
|
67
|
+
}
|
|
68
|
+
return current;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a single inline schema that may still have $ref (convenience helper
|
|
72
|
+
* used by other modules that receive already-partially-resolved specs).
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSchema(schema, root) {
|
|
75
|
+
return resolveRefs(schema, root);
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OpenApiSpec, ResourceMap } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parse the entire OpenAPI spec and produce one ResourceInfo per resource.
|
|
4
|
+
* A resource is a group of endpoints that share the same tag (or path prefix).
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildResourceMap(spec: OpenApiSpec): ResourceMap;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { pascalCase, camelCase } from 'change-case';
|
|
2
|
+
import { extractEndpoints } from './intent-detector.js';
|
|
3
|
+
import { mapFieldsFromSchema, buildZodSchema, mapColumnsFromSchema, } from './schema-field-mapper.js';
|
|
4
|
+
// ─── Naming helpers ───────────────────────────────────────────────────────────
|
|
5
|
+
/**
|
|
6
|
+
* Derive a plural connector composable name from a tag.
|
|
7
|
+
* 'pet' → 'usePetsConnector'
|
|
8
|
+
* 'store' → 'useStoreConnector' (already ends in e, avoid double-s)
|
|
9
|
+
*/
|
|
10
|
+
function toConnectorName(tag) {
|
|
11
|
+
const pascal = pascalCase(tag);
|
|
12
|
+
// Simple English plural: if ends in 's', 'x', 'z', 'ch', 'sh' → +es
|
|
13
|
+
// otherwise → +s. Good enough for API resource names.
|
|
14
|
+
const plural = /(?:s|x|z|ch|sh)$/i.test(pascal) ? `${pascal}es` : `${pascal}s`;
|
|
15
|
+
return `use${plural}Connector`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Primary grouping key for an endpoint: first tag, or path prefix as fallback.
|
|
19
|
+
* '/pets/{id}' → 'pets'
|
|
20
|
+
*/
|
|
21
|
+
function tagOrPrefix(endpoint) {
|
|
22
|
+
if (endpoint.tags.length > 0) {
|
|
23
|
+
return endpoint.tags[0];
|
|
24
|
+
}
|
|
25
|
+
// Path prefix: first non-empty segment, lower-cased
|
|
26
|
+
const segment = endpoint.path.split('/').find((s) => s && !s.startsWith('{'));
|
|
27
|
+
return segment ?? 'unknown';
|
|
28
|
+
}
|
|
29
|
+
// ─── Pick the "best" endpoint for each intent ─────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* When a resource has multiple endpoints with the same intent, pick the
|
|
32
|
+
* simplest one (fewest path params, then shortest path).
|
|
33
|
+
*
|
|
34
|
+
* Example: if both GET /pets and GET /users/{id}/pets detect as 'list',
|
|
35
|
+
* we prefer GET /pets (0 path params, shorter path).
|
|
36
|
+
*/
|
|
37
|
+
function pickBest(endpoints) {
|
|
38
|
+
return endpoints.sort((a, b) => a.pathParams.length - b.pathParams.length || a.path.length - b.path.length)[0];
|
|
39
|
+
}
|
|
40
|
+
// ─── Main grouper ─────────────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Parse the entire OpenAPI spec and produce one ResourceInfo per resource.
|
|
43
|
+
* A resource is a group of endpoints that share the same tag (or path prefix).
|
|
44
|
+
*/
|
|
45
|
+
export function buildResourceMap(spec) {
|
|
46
|
+
// 1. Collect all endpoints
|
|
47
|
+
const allEndpoints = [];
|
|
48
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
49
|
+
// pathItem is already $ref-resolved at this point
|
|
50
|
+
const endpoints = extractEndpoints(path, pathItem);
|
|
51
|
+
allEndpoints.push(...endpoints);
|
|
52
|
+
}
|
|
53
|
+
// 2. Group by tag / prefix
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
for (const ep of allEndpoints) {
|
|
56
|
+
const key = tagOrPrefix(ep);
|
|
57
|
+
if (!groups.has(key)) {
|
|
58
|
+
groups.set(key, []);
|
|
59
|
+
}
|
|
60
|
+
groups.get(key).push(ep);
|
|
61
|
+
}
|
|
62
|
+
// 3. Build one ResourceInfo per group
|
|
63
|
+
const resourceMap = new Map();
|
|
64
|
+
for (const [tag, endpoints] of groups) {
|
|
65
|
+
const byIntent = groupByIntent(endpoints);
|
|
66
|
+
const listEp = byIntent.list ? pickBest(byIntent.list) : undefined;
|
|
67
|
+
const detailEp = byIntent.detail ? pickBest(byIntent.detail) : undefined;
|
|
68
|
+
const createEp = byIntent.create ? pickBest(byIntent.create) : undefined;
|
|
69
|
+
const updateEp = byIntent.update ? pickBest(byIntent.update) : undefined;
|
|
70
|
+
const deleteEp = byIntent.delete ? pickBest(byIntent.delete) : undefined;
|
|
71
|
+
// Infer columns from list > detail response schema
|
|
72
|
+
const schemaForColumns = listEp?.responseSchema ?? detailEp?.responseSchema;
|
|
73
|
+
const columns = schemaForColumns ? mapColumnsFromSchema(schemaForColumns) : [];
|
|
74
|
+
// Form fields + Zod schemas
|
|
75
|
+
const createFields = createEp?.requestBodySchema
|
|
76
|
+
? mapFieldsFromSchema(createEp.requestBodySchema)
|
|
77
|
+
: undefined;
|
|
78
|
+
const updateFields = updateEp?.requestBodySchema
|
|
79
|
+
? mapFieldsFromSchema(updateEp.requestBodySchema)
|
|
80
|
+
: undefined;
|
|
81
|
+
const createZod = createEp?.requestBodySchema
|
|
82
|
+
? buildZodSchema(createEp.requestBodySchema)
|
|
83
|
+
: undefined;
|
|
84
|
+
const updateZod = updateEp?.requestBodySchema
|
|
85
|
+
? buildZodSchema(updateEp.requestBodySchema)
|
|
86
|
+
: undefined;
|
|
87
|
+
const resourceName = pascalCase(tag);
|
|
88
|
+
const info = {
|
|
89
|
+
name: resourceName,
|
|
90
|
+
tag,
|
|
91
|
+
composableName: toConnectorName(tag),
|
|
92
|
+
endpoints,
|
|
93
|
+
listEndpoint: listEp,
|
|
94
|
+
detailEndpoint: detailEp,
|
|
95
|
+
createEndpoint: createEp,
|
|
96
|
+
updateEndpoint: updateEp,
|
|
97
|
+
deleteEndpoint: deleteEp,
|
|
98
|
+
columns,
|
|
99
|
+
formFields: {
|
|
100
|
+
...(createFields ? { create: createFields } : {}),
|
|
101
|
+
...(updateFields ? { update: updateFields } : {}),
|
|
102
|
+
},
|
|
103
|
+
zodSchemas: {
|
|
104
|
+
...(createZod ? { create: createZod } : {}),
|
|
105
|
+
...(updateZod ? { update: updateZod } : {}),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
// Map key uses camelCase of the tag (e.g. 'petStore') to be a valid JS identifier.
|
|
109
|
+
// resource.name uses PascalCase ('PetStore') for use in type/class names.
|
|
110
|
+
// resource.tag preserves the original casing from the spec ('petStore' or 'pet_store').
|
|
111
|
+
resourceMap.set(camelCase(tag), info);
|
|
112
|
+
}
|
|
113
|
+
return resourceMap;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Group endpoints by their detected intent.
|
|
117
|
+
* Endpoints with intent 'unknown' (e.g. custom actions like POST /pets/{id}/upload)
|
|
118
|
+
* are silently skipped — they do not map to a standard CRUD connector.
|
|
119
|
+
*/
|
|
120
|
+
function groupByIntent(endpoints) {
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const ep of endpoints) {
|
|
123
|
+
if (ep.intent === 'unknown') {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!result[ep.intent]) {
|
|
127
|
+
result[ep.intent] = [];
|
|
128
|
+
}
|
|
129
|
+
result[ep.intent].push(ep);
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ColumnDef, FormFieldDef, OpenApiPropertySchema, OpenApiSchema } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Derive table ColumnDef[] from a response schema (list or detail).
|
|
4
|
+
* When the response schema is an array, we read the items schema.
|
|
5
|
+
*/
|
|
6
|
+
export declare function mapColumnsFromSchema(schema: OpenApiSchema): ColumnDef[];
|
|
7
|
+
/**
|
|
8
|
+
* Derive FormFieldDef[] from a request body schema.
|
|
9
|
+
* readOnly fields are included as hidden: true (developer can override).
|
|
10
|
+
*/
|
|
11
|
+
export declare function mapFieldsFromSchema(schema: OpenApiSchema): FormFieldDef[];
|
|
12
|
+
/**
|
|
13
|
+
* Generate a Zod expression string for a single OpenAPI property.
|
|
14
|
+
*
|
|
15
|
+
* Returns a SOURCE CODE STRING (e.g. 'z.string().min(3)') that will be
|
|
16
|
+
* embedded inside the generated connector file, not evaluated here.
|
|
17
|
+
*
|
|
18
|
+
* Validation constraints (minLength, maximum, enum…) are read directly
|
|
19
|
+
* from the OpenAPI property schema and chained onto the Zod expression.
|
|
20
|
+
*/
|
|
21
|
+
export declare function zodExpressionFromProp(prop: OpenApiPropertySchema, isRequired: boolean): string;
|
|
22
|
+
/**
|
|
23
|
+
* Build a complete z.object({...}) string from a request body schema.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ This returns a SOURCE CODE STRING, not a real Zod object.
|
|
26
|
+
* It is embedded verbatim inside the generated connector file so that
|
|
27
|
+
* the user's project (which has zod installed) can evaluate it at runtime.
|
|
28
|
+
*
|
|
29
|
+
* Example output:
|
|
30
|
+
* z.object({
|
|
31
|
+
* name: z.string().min(1),
|
|
32
|
+
* status: z.enum(['available','pending','sold']).optional(),
|
|
33
|
+
* })
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildZodSchema(schema: OpenApiSchema): string;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { pascalCase } from 'change-case';
|
|
2
|
+
// ─── Column mapping ───────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* Derive table ColumnDef[] from a response schema (list or detail).
|
|
5
|
+
* When the response schema is an array, we read the items schema.
|
|
6
|
+
*/
|
|
7
|
+
export function mapColumnsFromSchema(schema) {
|
|
8
|
+
// If the response is an array, inspect the items object schema
|
|
9
|
+
const objectSchema = schema.type === 'array' && schema.items ? schema.items : schema;
|
|
10
|
+
if (!objectSchema.properties) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
return Object.entries(objectSchema.properties).map(([key, prop]) => ({
|
|
14
|
+
key,
|
|
15
|
+
label: pascalCase(key)
|
|
16
|
+
.replace(/([A-Z])/g, ' $1')
|
|
17
|
+
.trim(),
|
|
18
|
+
type: columnTypeFromProp(prop),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function columnTypeFromProp(prop) {
|
|
22
|
+
if (prop.enum) {
|
|
23
|
+
return 'badge';
|
|
24
|
+
}
|
|
25
|
+
if (prop.type === 'boolean') {
|
|
26
|
+
return 'boolean';
|
|
27
|
+
}
|
|
28
|
+
if (prop.type === 'integer' || prop.type === 'number') {
|
|
29
|
+
return 'number';
|
|
30
|
+
}
|
|
31
|
+
if (prop.format === 'date' || prop.format === 'date-time') {
|
|
32
|
+
return 'date';
|
|
33
|
+
}
|
|
34
|
+
return 'text';
|
|
35
|
+
}
|
|
36
|
+
// ─── Form field mapping ───────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Derive FormFieldDef[] from a request body schema.
|
|
39
|
+
* readOnly fields are included as hidden: true (developer can override).
|
|
40
|
+
*/
|
|
41
|
+
export function mapFieldsFromSchema(schema) {
|
|
42
|
+
if (!schema.properties) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const required = new Set(schema.required ?? []);
|
|
46
|
+
return Object.entries(schema.properties).map(([key, prop]) => {
|
|
47
|
+
const isRequired = required.has(key);
|
|
48
|
+
const fieldType = fieldTypeFromProp(prop);
|
|
49
|
+
return {
|
|
50
|
+
key,
|
|
51
|
+
label: labelFromKey(key),
|
|
52
|
+
type: fieldType,
|
|
53
|
+
required: isRequired,
|
|
54
|
+
hidden: prop.readOnly === true,
|
|
55
|
+
options: optionsFromProp(prop),
|
|
56
|
+
placeholder: undefined,
|
|
57
|
+
zodExpression: zodExpressionFromProp(prop, isRequired),
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function labelFromKey(key) {
|
|
62
|
+
// Split camelCase/PascalCase into words and capitalise the first letter.
|
|
63
|
+
// 'photoUrls' → 'Photo Urls', 'firstName' → 'First Name'
|
|
64
|
+
return key
|
|
65
|
+
.replace(/([A-Z])/g, ' $1')
|
|
66
|
+
.replace(/^./, (c) => c.toUpperCase())
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
function fieldTypeFromProp(prop) {
|
|
70
|
+
if (prop.readOnly) {
|
|
71
|
+
return 'input'; // will be hidden anyway
|
|
72
|
+
}
|
|
73
|
+
if (prop.enum) {
|
|
74
|
+
return 'select';
|
|
75
|
+
}
|
|
76
|
+
if (prop.type === 'boolean') {
|
|
77
|
+
return 'checkbox';
|
|
78
|
+
}
|
|
79
|
+
if (prop.type === 'integer' || prop.type === 'number') {
|
|
80
|
+
return 'number';
|
|
81
|
+
}
|
|
82
|
+
if (prop.type === 'string') {
|
|
83
|
+
if (prop.format === 'date' || prop.format === 'date-time') {
|
|
84
|
+
return 'datepicker';
|
|
85
|
+
}
|
|
86
|
+
if (typeof prop.maxLength === 'number' && prop.maxLength > 200) {
|
|
87
|
+
return 'textarea';
|
|
88
|
+
}
|
|
89
|
+
return 'input';
|
|
90
|
+
}
|
|
91
|
+
// array, object → input as fallback
|
|
92
|
+
return 'input';
|
|
93
|
+
}
|
|
94
|
+
function optionsFromProp(prop) {
|
|
95
|
+
if (!prop.enum) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
return prop.enum.map((v) => ({ label: String(v), value: v }));
|
|
99
|
+
}
|
|
100
|
+
// ─── Zod expression generation ────────────────────────────────────────────────
|
|
101
|
+
/**
|
|
102
|
+
* Generate a Zod expression string for a single OpenAPI property.
|
|
103
|
+
*
|
|
104
|
+
* Returns a SOURCE CODE STRING (e.g. 'z.string().min(3)') that will be
|
|
105
|
+
* embedded inside the generated connector file, not evaluated here.
|
|
106
|
+
*
|
|
107
|
+
* Validation constraints (minLength, maximum, enum…) are read directly
|
|
108
|
+
* from the OpenAPI property schema and chained onto the Zod expression.
|
|
109
|
+
*/
|
|
110
|
+
export function zodExpressionFromProp(prop, isRequired) {
|
|
111
|
+
let expr = baseZodExpr(prop);
|
|
112
|
+
if (!isRequired) {
|
|
113
|
+
expr += '.optional()';
|
|
114
|
+
}
|
|
115
|
+
return expr;
|
|
116
|
+
}
|
|
117
|
+
function baseZodExpr(prop) {
|
|
118
|
+
// Enum
|
|
119
|
+
if (prop.enum && prop.enum.length > 0) {
|
|
120
|
+
const values = prop.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
121
|
+
return `z.enum([${values}])`;
|
|
122
|
+
}
|
|
123
|
+
switch (prop.type) {
|
|
124
|
+
case 'string':
|
|
125
|
+
return stringZodExpr(prop);
|
|
126
|
+
case 'integer':
|
|
127
|
+
return integerZodExpr(prop);
|
|
128
|
+
case 'number':
|
|
129
|
+
return numberZodExpr(prop);
|
|
130
|
+
case 'boolean':
|
|
131
|
+
return 'z.boolean()';
|
|
132
|
+
case 'array':
|
|
133
|
+
return arrayZodExpr(prop);
|
|
134
|
+
case 'object':
|
|
135
|
+
return 'z.record(z.unknown())';
|
|
136
|
+
default:
|
|
137
|
+
// $ref already resolved, unknown type → permissive
|
|
138
|
+
return 'z.unknown()';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function stringZodExpr(prop) {
|
|
142
|
+
const expr = 'z.string()';
|
|
143
|
+
if (prop.format === 'email') {
|
|
144
|
+
return `${expr}.email()`;
|
|
145
|
+
}
|
|
146
|
+
if (prop.format === 'uri' || prop.format === 'url') {
|
|
147
|
+
return `${expr}.url()`;
|
|
148
|
+
}
|
|
149
|
+
if (prop.format === 'uuid') {
|
|
150
|
+
return `${expr}.uuid()`;
|
|
151
|
+
}
|
|
152
|
+
if (prop.format === 'date' || prop.format === 'date-time') {
|
|
153
|
+
return `${expr}.datetime()`;
|
|
154
|
+
}
|
|
155
|
+
let chained = expr;
|
|
156
|
+
if (typeof prop.minLength === 'number') {
|
|
157
|
+
chained += `.min(${prop.minLength})`;
|
|
158
|
+
}
|
|
159
|
+
if (typeof prop.maxLength === 'number') {
|
|
160
|
+
chained += `.max(${prop.maxLength})`;
|
|
161
|
+
}
|
|
162
|
+
return chained;
|
|
163
|
+
}
|
|
164
|
+
function integerZodExpr(prop) {
|
|
165
|
+
let expr = 'z.number().int()';
|
|
166
|
+
if (typeof prop.minimum === 'number') {
|
|
167
|
+
expr += `.min(${prop.minimum})`;
|
|
168
|
+
}
|
|
169
|
+
if (typeof prop.maximum === 'number') {
|
|
170
|
+
expr += `.max(${prop.maximum})`;
|
|
171
|
+
}
|
|
172
|
+
return expr;
|
|
173
|
+
}
|
|
174
|
+
function numberZodExpr(prop) {
|
|
175
|
+
let expr = 'z.number()';
|
|
176
|
+
if (typeof prop.minimum === 'number') {
|
|
177
|
+
expr += `.min(${prop.minimum})`;
|
|
178
|
+
}
|
|
179
|
+
if (typeof prop.maximum === 'number') {
|
|
180
|
+
expr += `.max(${prop.maximum})`;
|
|
181
|
+
}
|
|
182
|
+
return expr;
|
|
183
|
+
}
|
|
184
|
+
function arrayZodExpr(prop) {
|
|
185
|
+
const itemExpr = prop.items ? baseZodExpr(prop.items) : 'z.unknown()';
|
|
186
|
+
let expr = `z.array(${itemExpr})`;
|
|
187
|
+
if (typeof prop.minItems === 'number') {
|
|
188
|
+
expr += `.min(${prop.minItems})`;
|
|
189
|
+
}
|
|
190
|
+
if (typeof prop.maxItems === 'number') {
|
|
191
|
+
expr += `.max(${prop.maxItems})`;
|
|
192
|
+
}
|
|
193
|
+
return expr;
|
|
194
|
+
}
|
|
195
|
+
// ─── Full Zod object schema string ───────────────────────────────────────────
|
|
196
|
+
/**
|
|
197
|
+
* Build a complete z.object({...}) string from a request body schema.
|
|
198
|
+
*
|
|
199
|
+
* ⚠️ This returns a SOURCE CODE STRING, not a real Zod object.
|
|
200
|
+
* It is embedded verbatim inside the generated connector file so that
|
|
201
|
+
* the user's project (which has zod installed) can evaluate it at runtime.
|
|
202
|
+
*
|
|
203
|
+
* Example output:
|
|
204
|
+
* z.object({
|
|
205
|
+
* name: z.string().min(1),
|
|
206
|
+
* status: z.enum(['available','pending','sold']).optional(),
|
|
207
|
+
* })
|
|
208
|
+
*/
|
|
209
|
+
export function buildZodSchema(schema) {
|
|
210
|
+
if (!schema.properties) {
|
|
211
|
+
return 'z.object({})';
|
|
212
|
+
}
|
|
213
|
+
const required = new Set(schema.required ?? []);
|
|
214
|
+
const lines = Object.entries(schema.properties).map(([key, prop]) => {
|
|
215
|
+
const expr = zodExpressionFromProp(prop, required.has(key));
|
|
216
|
+
const readOnly = prop.readOnly ? ' // readOnly — excluded from form' : '';
|
|
217
|
+
return ` ${key}: ${expr},${readOnly}`;
|
|
218
|
+
});
|
|
219
|
+
return `z.object({\n${lines.join('\n')}\n})`;
|
|
220
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Schema Analyzer — Fase 1 of the Connector Generator.
|
|
3
|
+
*
|
|
4
|
+
* The Schema Analyzer reads an OpenAPI YAML/JSON spec directly and produces
|
|
5
|
+
* a ResourceMap: one ResourceInfo per tag/path-prefix group of endpoints.
|
|
6
|
+
*/
|
|
7
|
+
export type Intent = 'list' | 'detail' | 'create' | 'update' | 'delete' | 'unknown';
|
|
8
|
+
export interface OpenApiPropertySchema {
|
|
9
|
+
type?: string;
|
|
10
|
+
format?: string;
|
|
11
|
+
enum?: string[];
|
|
12
|
+
items?: OpenApiPropertySchema;
|
|
13
|
+
$ref?: string;
|
|
14
|
+
readOnly?: boolean;
|
|
15
|
+
writeOnly?: boolean;
|
|
16
|
+
minLength?: number;
|
|
17
|
+
maxLength?: number;
|
|
18
|
+
minimum?: number;
|
|
19
|
+
maximum?: number;
|
|
20
|
+
minItems?: number;
|
|
21
|
+
maxItems?: number;
|
|
22
|
+
properties?: Record<string, OpenApiPropertySchema>;
|
|
23
|
+
required?: string[];
|
|
24
|
+
description?: string;
|
|
25
|
+
example?: unknown;
|
|
26
|
+
additionalProperties?: OpenApiPropertySchema | boolean;
|
|
27
|
+
allOf?: OpenApiPropertySchema[];
|
|
28
|
+
oneOf?: OpenApiPropertySchema[];
|
|
29
|
+
anyOf?: OpenApiPropertySchema[];
|
|
30
|
+
}
|
|
31
|
+
export interface OpenApiSchema extends OpenApiPropertySchema {
|
|
32
|
+
required?: string[];
|
|
33
|
+
properties?: Record<string, OpenApiPropertySchema>;
|
|
34
|
+
}
|
|
35
|
+
export interface OpenApiParameter {
|
|
36
|
+
name: string;
|
|
37
|
+
in: 'path' | 'query' | 'header' | 'cookie';
|
|
38
|
+
required?: boolean;
|
|
39
|
+
schema?: OpenApiPropertySchema;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface OpenApiOperation {
|
|
43
|
+
operationId?: string;
|
|
44
|
+
tags?: string[];
|
|
45
|
+
summary?: string;
|
|
46
|
+
description?: string;
|
|
47
|
+
parameters?: OpenApiParameter[];
|
|
48
|
+
requestBody?: {
|
|
49
|
+
required?: boolean;
|
|
50
|
+
content?: Record<string, {
|
|
51
|
+
schema?: OpenApiPropertySchema;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
responses?: Record<string, {
|
|
55
|
+
description?: string;
|
|
56
|
+
content?: Record<string, {
|
|
57
|
+
schema?: OpenApiPropertySchema;
|
|
58
|
+
}>;
|
|
59
|
+
}>;
|
|
60
|
+
/** Developer override — detected intent for this endpoint */
|
|
61
|
+
'x-nxh-intent'?: Intent;
|
|
62
|
+
}
|
|
63
|
+
export interface OpenApiPathItem {
|
|
64
|
+
get?: OpenApiOperation;
|
|
65
|
+
post?: OpenApiOperation;
|
|
66
|
+
put?: OpenApiOperation;
|
|
67
|
+
patch?: OpenApiOperation;
|
|
68
|
+
delete?: OpenApiOperation;
|
|
69
|
+
}
|
|
70
|
+
export interface OpenApiSpec {
|
|
71
|
+
openapi: string;
|
|
72
|
+
info: {
|
|
73
|
+
title: string;
|
|
74
|
+
version: string;
|
|
75
|
+
};
|
|
76
|
+
tags?: Array<{
|
|
77
|
+
name: string;
|
|
78
|
+
description?: string;
|
|
79
|
+
}>;
|
|
80
|
+
paths: Record<string, OpenApiPathItem>;
|
|
81
|
+
components?: {
|
|
82
|
+
schemas?: Record<string, OpenApiPropertySchema>;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface EndpointInfo {
|
|
86
|
+
operationId: string;
|
|
87
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
88
|
+
path: string;
|
|
89
|
+
tags: string[];
|
|
90
|
+
summary?: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
intent: Intent;
|
|
93
|
+
/** Resolved (no $ref) request body schema for POST/PUT/PATCH */
|
|
94
|
+
requestBodySchema?: OpenApiSchema;
|
|
95
|
+
/** Resolved (no $ref) successful response schema (first 2xx) */
|
|
96
|
+
responseSchema?: OpenApiSchema;
|
|
97
|
+
hasPathParams: boolean;
|
|
98
|
+
pathParams: string[];
|
|
99
|
+
}
|
|
100
|
+
export type FieldType = 'input' | 'textarea' | 'select' | 'checkbox' | 'datepicker' | 'number';
|
|
101
|
+
export interface FormFieldDef {
|
|
102
|
+
key: string;
|
|
103
|
+
label: string;
|
|
104
|
+
type: FieldType;
|
|
105
|
+
required: boolean;
|
|
106
|
+
options?: {
|
|
107
|
+
label: string;
|
|
108
|
+
value: string;
|
|
109
|
+
}[];
|
|
110
|
+
placeholder?: string;
|
|
111
|
+
hidden?: boolean;
|
|
112
|
+
/** Zod expression string, e.g. 'z.string().min(1)' */
|
|
113
|
+
zodExpression: string;
|
|
114
|
+
}
|
|
115
|
+
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'badge';
|
|
116
|
+
export interface ColumnDef {
|
|
117
|
+
key: string;
|
|
118
|
+
label: string;
|
|
119
|
+
type: ColumnType;
|
|
120
|
+
}
|
|
121
|
+
export interface ResourceInfo {
|
|
122
|
+
/** Normalized resource name, PascalCase. E.g. 'Pet', 'Store' */
|
|
123
|
+
name: string;
|
|
124
|
+
/** Tag name as it appears in the spec. E.g. 'pet', 'store' */
|
|
125
|
+
tag: string;
|
|
126
|
+
/** Generated connector composable name. E.g. 'usePetsConnector' */
|
|
127
|
+
composableName: string;
|
|
128
|
+
endpoints: EndpointInfo[];
|
|
129
|
+
/** The one endpoint detected as list (GET array) */
|
|
130
|
+
listEndpoint?: EndpointInfo;
|
|
131
|
+
/** The one endpoint detected as detail (GET single object) */
|
|
132
|
+
detailEndpoint?: EndpointInfo;
|
|
133
|
+
/** The one endpoint detected as create (POST) */
|
|
134
|
+
createEndpoint?: EndpointInfo;
|
|
135
|
+
/** The one endpoint detected as update (PUT/PATCH) */
|
|
136
|
+
updateEndpoint?: EndpointInfo;
|
|
137
|
+
/** The one endpoint detected as delete (DELETE) */
|
|
138
|
+
deleteEndpoint?: EndpointInfo;
|
|
139
|
+
/** Columns inferred from the list/detail response schema */
|
|
140
|
+
columns: ColumnDef[];
|
|
141
|
+
/** Form fields inferred from the request body schema */
|
|
142
|
+
formFields: {
|
|
143
|
+
create?: FormFieldDef[];
|
|
144
|
+
update?: FormFieldDef[];
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Zod schema object expression strings, ready to embed in generated code.
|
|
148
|
+
* E.g. "z.object({\n name: z.string().min(1),\n ....\n})"
|
|
149
|
+
*/
|
|
150
|
+
zodSchemas: {
|
|
151
|
+
create?: string;
|
|
152
|
+
update?: string;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** One entry per tag (or path-prefix fallback) */
|
|
156
|
+
export type ResourceMap = Map<string, ResourceInfo>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type Logger } from '../../cli/logger.js';
|
|
1
2
|
/**
|
|
2
3
|
* Main function to generate Nuxt Server Routes
|
|
3
4
|
*/
|
|
4
5
|
export declare function generateNuxtServerRoutes(inputDir: string, serverRoutePath: string, options?: {
|
|
5
6
|
enableBff?: boolean;
|
|
6
7
|
backend?: string;
|
|
7
|
-
}): Promise<void>;
|
|
8
|
+
}, logger?: Logger): Promise<void>;
|