@pikku/cli 0.12.1 → 0.12.3
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/cli.schema.json +1 -1
- package/console-app/assets/index-Ci24-VT-.js +657 -0
- package/console-app/assets/{index-0Ui5UudO.css → index-DvrDbftC.css} +1 -1
- package/console-app/index.html +2 -2
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-agent-wirings-meta.gen.js +1 -1
- package/dist/.pikku/agent/pikku-agent-wirings.gen.d.ts +1 -1
- package/dist/.pikku/agent/pikku-agent-wirings.gen.js +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/channel/pikku-channels-meta.gen.js +1 -1
- package/dist/.pikku/channel/pikku-channels.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channels.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.d.ts +13 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +37 -2
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +64 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +30 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +17 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +155 -76
- package/dist/.pikku/function/pikku-functions.gen.js +3 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-wirings-meta.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-wirings.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-wirings.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +3 -1
- package/dist/.pikku/pikku-services.gen.js +2 -0
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/pikku-websocket.gen.d.ts +1 -1
- package/dist/.pikku/pikku-websocket.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-remote-rpc-workers.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +10 -5
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/scheduler/pikku-schedulers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-schedulers-wirings.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +9 -7
- package/dist/.pikku/schemas/schemas/ConsoleCommandInput.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuGatewayOutput.schema.json +1 -0
- package/dist/.pikku/schemas/schemas/PikkuNewAddonInput.schema.json +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/src/cli.wiring.js +55 -0
- package/dist/src/functions/commands/all.js +40 -13
- package/dist/src/functions/commands/console.d.ts +3 -0
- package/dist/src/functions/commands/console.js +4 -2
- package/dist/src/functions/commands/enable.d.ts +4 -0
- package/dist/src/functions/commands/enable.js +39 -0
- package/dist/src/functions/commands/new-addon.d.ts +3 -0
- package/dist/src/functions/commands/new-addon.js +8 -6
- package/dist/src/functions/runtimes/fetch/index.js +2 -1
- package/dist/src/functions/runtimes/nextjs/pikku-command-nextjs.js +4 -1
- package/dist/src/functions/runtimes/websocket/pikku-command-websocket-typed.js +2 -1
- package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +3 -5
- package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +19 -0
- package/dist/src/functions/wirings/channels/pikku-command-channels-map.js +1 -1
- package/dist/src/functions/wirings/channels/serialize-typed-channel-map.d.ts +1 -1
- package/dist/src/functions/wirings/channels/serialize-typed-channel-map.js +7 -6
- package/dist/src/functions/wirings/cli/pikku-command-cli-entry.js +9 -1
- package/dist/src/functions/wirings/cli/serialize-channel-cli.js +35 -12
- package/dist/src/functions/wirings/console/pikku-command-console-functions.js +4 -6
- package/dist/src/functions/wirings/console/pikku-command-node-types.js +2 -2
- package/dist/src/functions/wirings/console/pikku-command-nodes-meta.js +20 -11
- package/dist/src/functions/wirings/console/serialize-console-functions.js +1 -1
- package/dist/src/functions/wirings/functions/pikku-command-function-types-split.js +1 -1
- package/dist/src/functions/wirings/functions/serialize-addon-types.js +23 -1
- package/dist/src/functions/wirings/functions/serialize-function-types.js +38 -0
- package/dist/src/functions/wirings/gateway/pikku-command-gateway.d.ts +1 -0
- package/dist/src/functions/wirings/gateway/pikku-command-gateway.js +22 -0
- package/dist/src/functions/wirings/mcp/pikku-command-mcp-json.js +1 -1
- package/dist/src/functions/wirings/queue/pikku-command-queue-service.js +2 -1
- package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +3 -5
- package/dist/src/functions/wirings/rpc/pikku-command-remote-rpc.js +3 -5
- package/dist/src/functions/wirings/rpc/pikku-command-rpc-client.js +2 -1
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +3 -10
- package/dist/src/services.js +13 -1
- package/dist/src/utils/file-import-path.js +5 -2
- package/dist/src/utils/openapi/codegen.d.ts +1 -0
- package/dist/src/utils/openapi/codegen.js +214 -46
- package/dist/src/utils/openapi/parse-openapi.d.ts +25 -0
- package/dist/src/utils/openapi/parse-openapi.js +119 -9
- package/dist/src/utils/openapi/zod-codegen.d.ts +1 -53
- package/dist/src/utils/openapi/zod-codegen.js +1 -251
- package/dist/src/utils/pikku-cli-config.js +45 -18
- package/dist/src/utils/strip-verbose-meta.d.ts +2 -0
- package/dist/src/utils/strip-verbose-meta.js +34 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/console-app/assets/index-DiYPTQU_.js +0 -676
|
@@ -30,8 +30,11 @@ export const getFileImportRelativePath = (from, to, packageMappings) => {
|
|
|
30
30
|
const fromAbsolutePath = resolve(dirname(from));
|
|
31
31
|
// Check if both files are in the same package directory
|
|
32
32
|
// If so, skip packageMappings to use relative paths
|
|
33
|
+
// Skip '.' key — it matches any dot character in paths and is too generic for substring matching
|
|
33
34
|
let inSamePackage = false;
|
|
34
35
|
for (const [path] of Object.entries(packageMappings)) {
|
|
36
|
+
if (path === '.')
|
|
37
|
+
continue;
|
|
35
38
|
if (absolutePath.includes(path) && fromAbsolutePath.includes(path)) {
|
|
36
39
|
inSamePackage = true;
|
|
37
40
|
break;
|
|
@@ -39,10 +42,10 @@ export const getFileImportRelativePath = (from, to, packageMappings) => {
|
|
|
39
42
|
}
|
|
40
43
|
// Only apply packageMappings if files are not in the same package
|
|
41
44
|
if (!inSamePackage) {
|
|
42
|
-
// let usesPackageName = false
|
|
43
45
|
for (const [path, packageName] of Object.entries(packageMappings)) {
|
|
46
|
+
if (path === '.')
|
|
47
|
+
continue;
|
|
44
48
|
if (absolutePath.includes(path)) {
|
|
45
|
-
// usesPackageName = true
|
|
46
49
|
// Use string slicing instead of regex to avoid ReDoS and ensure correct behavior
|
|
47
50
|
const pathIndex = absolutePath.indexOf(path);
|
|
48
51
|
filePath = packageName + absolutePath.slice(pathIndex + path.length);
|
|
@@ -1,21 +1,48 @@
|
|
|
1
1
|
import { schemaToZod, schemaVarName, createContext, } from './zod-codegen.js';
|
|
2
2
|
import { generateOperationNames, detectCommonPrefix, } from './naming.js';
|
|
3
3
|
const GENERIC_SUMMARIES = new Set([
|
|
4
|
-
'index',
|
|
4
|
+
'index',
|
|
5
|
+
'show',
|
|
6
|
+
'create',
|
|
7
|
+
'update',
|
|
8
|
+
'destroy',
|
|
9
|
+
'delete',
|
|
10
|
+
'list',
|
|
5
11
|
]);
|
|
12
|
+
/** Map from HTTP status code to pikku error class name */
|
|
13
|
+
const STATUS_TO_ERROR = {
|
|
14
|
+
400: 'BadRequestError',
|
|
15
|
+
401: 'UnauthorizedError',
|
|
16
|
+
403: 'ForbiddenError',
|
|
17
|
+
404: 'NotFoundError',
|
|
18
|
+
405: 'MethodNotAllowedError',
|
|
19
|
+
409: 'ConflictError',
|
|
20
|
+
422: 'UnprocessableContentError',
|
|
21
|
+
429: 'TooManyRequestsError',
|
|
22
|
+
500: 'InternalServerError',
|
|
23
|
+
};
|
|
6
24
|
function capitalize(str) {
|
|
7
25
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
8
26
|
}
|
|
9
27
|
function humanDescription(named, parsed) {
|
|
10
|
-
if (parsed.responseDescription) {
|
|
11
|
-
return capitalize(parsed.responseDescription);
|
|
12
|
-
}
|
|
13
28
|
const summary = parsed.summary?.trim();
|
|
29
|
+
const description = parsed.description?.trim();
|
|
30
|
+
// Summary is preferred — it's the operation's intent
|
|
14
31
|
if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
|
|
32
|
+
// If description adds info beyond summary, combine them
|
|
33
|
+
if (description &&
|
|
34
|
+
!GENERIC_SUMMARIES.has(description.toLowerCase()) &&
|
|
35
|
+
description.toLowerCase() !== summary.toLowerCase()) {
|
|
36
|
+
const sep = summary.endsWith('.') ? ' ' : '. ';
|
|
37
|
+
return `${capitalize(summary)}${sep}${capitalize(description)}`;
|
|
38
|
+
}
|
|
15
39
|
return capitalize(summary);
|
|
16
40
|
}
|
|
17
|
-
if (
|
|
18
|
-
return capitalize(
|
|
41
|
+
if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
|
|
42
|
+
return capitalize(description);
|
|
43
|
+
}
|
|
44
|
+
if (parsed.responseDescription) {
|
|
45
|
+
return capitalize(parsed.responseDescription);
|
|
19
46
|
}
|
|
20
47
|
const words = named.functionName
|
|
21
48
|
.replace(/([A-Z])/g, ' $1')
|
|
@@ -23,6 +50,16 @@ function humanDescription(named, parsed) {
|
|
|
23
50
|
.toLowerCase();
|
|
24
51
|
return capitalize(words);
|
|
25
52
|
}
|
|
53
|
+
function getErrorClassesForResponses(errorResponses) {
|
|
54
|
+
const classes = [];
|
|
55
|
+
for (const err of errorResponses) {
|
|
56
|
+
const cls = STATUS_TO_ERROR[err.statusCode];
|
|
57
|
+
if (cls && !classes.includes(cls)) {
|
|
58
|
+
classes.push(cls);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return classes;
|
|
62
|
+
}
|
|
26
63
|
export function generateAddonFromOpenAPI(spec, vars, flags) {
|
|
27
64
|
const files = {};
|
|
28
65
|
const { name } = vars;
|
|
@@ -50,7 +87,7 @@ export function generateAddonFromOpenAPI(spec, vars, flags) {
|
|
|
50
87
|
const functionExports = [];
|
|
51
88
|
for (const { named, parsed } of opPairs) {
|
|
52
89
|
const funcCtx = createContext(schemaRefs);
|
|
53
|
-
const funcCode = generateFunctionFile(named, parsed, vars, funcCtx);
|
|
90
|
+
const funcCode = generateFunctionFile(named, parsed, vars, funcCtx, spec, flags);
|
|
54
91
|
files[`src/functions/${named.functionName}.function.ts`] = funcCode;
|
|
55
92
|
functionExports.push(named.functionName);
|
|
56
93
|
}
|
|
@@ -58,6 +95,8 @@ export function generateAddonFromOpenAPI(spec, vars, flags) {
|
|
|
58
95
|
files['src/index.ts'] = generateIndexFile(functionExports);
|
|
59
96
|
// Generate typed API service class with route map
|
|
60
97
|
files[`src/${name}-api.service.ts`] = generateServiceFile(spec, opPairs, vars, flags);
|
|
98
|
+
// Generate variable file for BASE_URL
|
|
99
|
+
files[`src/${name}.variable.ts`] = generateVariableFile(spec, vars);
|
|
61
100
|
return files;
|
|
62
101
|
}
|
|
63
102
|
function generateTypesFile(spec, ctx) {
|
|
@@ -75,17 +114,32 @@ function generateTypesFile(spec, ctx) {
|
|
|
75
114
|
}
|
|
76
115
|
return lines.join('\n');
|
|
77
116
|
}
|
|
78
|
-
function generateFunctionFile(named, parsed, vars, ctx) {
|
|
117
|
+
function generateFunctionFile(named, parsed, vars, ctx, spec, flags) {
|
|
79
118
|
const lines = [];
|
|
80
119
|
const { camelName } = vars;
|
|
120
|
+
// Tag description as file header
|
|
121
|
+
if (parsed.tags.length > 0) {
|
|
122
|
+
const tag = parsed.tags[0];
|
|
123
|
+
const tagDesc = spec.tagDescriptions[tag];
|
|
124
|
+
if (tagDesc) {
|
|
125
|
+
lines.push(`// ${tag} — ${tagDesc}`);
|
|
126
|
+
lines.push('');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
81
129
|
const hasInput = parsed.pathParams.length > 0 ||
|
|
82
130
|
parsed.queryParams.length > 0 ||
|
|
131
|
+
parsed.headerParams.length > 0 ||
|
|
83
132
|
parsed.requestBody;
|
|
84
133
|
const pascalName = named.functionName.charAt(0).toUpperCase() + named.functionName.slice(1);
|
|
85
134
|
const inputName = `${pascalName}Input`;
|
|
86
135
|
const outputName = `${pascalName}Output`;
|
|
136
|
+
// Determine error imports needed
|
|
137
|
+
const errorClasses = getErrorClassesForResponses(parsed.errorResponses);
|
|
87
138
|
lines.push("import { z } from 'zod'");
|
|
88
139
|
lines.push("import { pikkuSessionlessFunc } from '#pikku'");
|
|
140
|
+
if (errorClasses.length > 0) {
|
|
141
|
+
lines.push(`import { ${errorClasses.join(', ')} } from '@pikku/core/errors'`);
|
|
142
|
+
}
|
|
89
143
|
lines.push('');
|
|
90
144
|
// Build Input schema (exported for pikku schema discovery)
|
|
91
145
|
if (hasInput) {
|
|
@@ -95,7 +149,7 @@ function generateFunctionFile(named, parsed, vars, ctx) {
|
|
|
95
149
|
}
|
|
96
150
|
// Build Output schema (exported for pikku schema discovery)
|
|
97
151
|
if (parsed.responseSchema) {
|
|
98
|
-
const outputCode =
|
|
152
|
+
const outputCode = buildOutputSchema(parsed.responseSchema, ctx);
|
|
99
153
|
lines.push(`export const ${outputName} = ${outputCode}`);
|
|
100
154
|
lines.push('');
|
|
101
155
|
}
|
|
@@ -105,11 +159,19 @@ function generateFunctionFile(named, parsed, vars, ctx) {
|
|
|
105
159
|
funcConfig.push(` description: ${JSON.stringify(description)},`);
|
|
106
160
|
if (hasInput)
|
|
107
161
|
funcConfig.push(` input: ${inputName},`);
|
|
108
|
-
if (parsed.responseSchema)
|
|
162
|
+
if (parsed.responseSchema) {
|
|
109
163
|
funcConfig.push(` output: ${outputName},`);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
:
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
funcConfig.push(' output: z.void(),');
|
|
167
|
+
}
|
|
168
|
+
if (errorClasses.length > 0) {
|
|
169
|
+
funcConfig.push(` errors: [${errorClasses.join(', ')}],`);
|
|
170
|
+
}
|
|
171
|
+
if (flags.mcp) {
|
|
172
|
+
funcConfig.push(' mcp: true,');
|
|
173
|
+
}
|
|
174
|
+
const funcParams = hasInput ? `{ ${camelName} }, data` : `{ ${camelName} }`;
|
|
113
175
|
const returnCast = parsed.responseSchema ? ' as any' : '';
|
|
114
176
|
funcConfig.push(` func: async (${funcParams}) => {`, ` return ${camelName}.call('${method}', '${parsed.path}'${hasInput ? ', data' : ''})${returnCast}`, ' },');
|
|
115
177
|
lines.push(`export const ${named.functionName} = pikkuSessionlessFunc({`);
|
|
@@ -121,26 +183,27 @@ function generateFunctionFile(named, parsed, vars, ctx) {
|
|
|
121
183
|
function buildInputSchema(parsed, ctx) {
|
|
122
184
|
const props = [];
|
|
123
185
|
for (const param of parsed.pathParams) {
|
|
124
|
-
|
|
125
|
-
const desc = param.description
|
|
126
|
-
? `${zodCode}.describe(${JSON.stringify(param.description)})`
|
|
127
|
-
: zodCode;
|
|
128
|
-
props.push(` ${param.name}: ${desc},`);
|
|
186
|
+
props.push(formatParamProp(param, ctx));
|
|
129
187
|
}
|
|
130
188
|
for (const param of parsed.queryParams) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
props.push(` ${param.name}: ${desc},`);
|
|
189
|
+
props.push(formatParamProp(param, ctx));
|
|
190
|
+
}
|
|
191
|
+
for (const param of parsed.headerParams) {
|
|
192
|
+
props.push(formatParamProp(param, ctx));
|
|
136
193
|
}
|
|
137
194
|
if (parsed.requestBody) {
|
|
138
195
|
if (parsed.requestBody.properties) {
|
|
139
196
|
const requiredSet = new Set(parsed.requestBody.required ?? []);
|
|
140
197
|
for (const [key, propSchema] of Object.entries(parsed.requestBody.properties)) {
|
|
198
|
+
// Skip readOnly properties from input
|
|
199
|
+
if (propSchema.readOnly)
|
|
200
|
+
continue;
|
|
141
201
|
const isOptional = !requiredSet.has(key);
|
|
142
202
|
const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
|
|
143
|
-
|
|
203
|
+
const withDesc = propSchema.description
|
|
204
|
+
? `${zodCode}.describe(${JSON.stringify(propSchema.description)})`
|
|
205
|
+
: zodCode;
|
|
206
|
+
props.push(` ${key}: ${withDesc},`);
|
|
144
207
|
}
|
|
145
208
|
}
|
|
146
209
|
else {
|
|
@@ -150,6 +213,32 @@ function buildInputSchema(parsed, ctx) {
|
|
|
150
213
|
}
|
|
151
214
|
return `z.object({\n${props.join('\n')}\n})`;
|
|
152
215
|
}
|
|
216
|
+
function formatParamProp(param, ctx) {
|
|
217
|
+
const zodCode = schemaToZod(param.schema, ctx, { optional: !param.required });
|
|
218
|
+
let descParts = [];
|
|
219
|
+
if (param.description)
|
|
220
|
+
descParts.push(param.description);
|
|
221
|
+
if (param.example !== undefined)
|
|
222
|
+
descParts.push(`Example: ${JSON.stringify(param.example)}`);
|
|
223
|
+
const desc = descParts.length > 0
|
|
224
|
+
? `${zodCode}.describe(${JSON.stringify(descParts.join('. '))})`
|
|
225
|
+
: zodCode;
|
|
226
|
+
return ` ${param.name}: ${desc},`;
|
|
227
|
+
}
|
|
228
|
+
function buildOutputSchema(schema, ctx) {
|
|
229
|
+
// For output schemas, filter out writeOnly properties
|
|
230
|
+
if (schema.properties) {
|
|
231
|
+
const filteredProps = {};
|
|
232
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
233
|
+
if (!propSchema.writeOnly) {
|
|
234
|
+
filteredProps[key] = propSchema;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const filteredSchema = { ...schema, properties: filteredProps };
|
|
238
|
+
return schemaToZod(filteredSchema, ctx);
|
|
239
|
+
}
|
|
240
|
+
return schemaToZod(schema, ctx);
|
|
241
|
+
}
|
|
153
242
|
function generateIndexFile(functionExports) {
|
|
154
243
|
const lines = [];
|
|
155
244
|
for (const name of functionExports) {
|
|
@@ -161,7 +250,8 @@ function generateIndexFile(functionExports) {
|
|
|
161
250
|
function generateServiceFile(spec, opPairs, vars, flags) {
|
|
162
251
|
const { name, pascalName, screamingName, displayName } = vars;
|
|
163
252
|
const lines = [];
|
|
164
|
-
|
|
253
|
+
// Always import all error classes used in the switch statement
|
|
254
|
+
const allErrorClasses = new Set(Object.values(STATUS_TO_ERROR));
|
|
165
255
|
if (flags.oauth) {
|
|
166
256
|
lines.push("import { OAuth2Client } from '@pikku/core/oauth2'");
|
|
167
257
|
lines.push("import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'");
|
|
@@ -169,15 +259,25 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
169
259
|
else if (flags.secret) {
|
|
170
260
|
lines.push(`import type { ${pascalName}Secrets } from './${name}.secret.js'`);
|
|
171
261
|
}
|
|
172
|
-
|
|
173
|
-
|
|
262
|
+
if (allErrorClasses.size > 0) {
|
|
263
|
+
lines.push(`import { ${[...allErrorClasses].sort().join(', ')} } from '@pikku/core/errors'`);
|
|
264
|
+
}
|
|
265
|
+
lines.push(`import type { TypedVariablesService } from '#pikku/variables/pikku-variables.gen.js'`);
|
|
174
266
|
lines.push('');
|
|
175
267
|
if (flags.oauth) {
|
|
268
|
+
// Use OAuth2 details from spec if available
|
|
269
|
+
const oauthScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'oauth2');
|
|
270
|
+
const authUrl = oauthScheme?.flows?.authorizationUrl ??
|
|
271
|
+
'https://example.com/oauth2/authorize';
|
|
272
|
+
const tokenUrl = oauthScheme?.flows?.tokenUrl ?? 'https://example.com/oauth2/token';
|
|
273
|
+
const scopes = oauthScheme?.flows?.scopes
|
|
274
|
+
? Object.keys(oauthScheme.flows.scopes)
|
|
275
|
+
: ['read', 'write'];
|
|
176
276
|
lines.push(`export const ${screamingName}_OAUTH2_CONFIG = {`);
|
|
177
277
|
lines.push(` tokenSecretId: '${screamingName}_TOKENS',`);
|
|
178
|
-
lines.push(` authorizationUrl:
|
|
179
|
-
lines.push(` tokenUrl:
|
|
180
|
-
lines.push(
|
|
278
|
+
lines.push(` authorizationUrl: ${JSON.stringify(authUrl)},`);
|
|
279
|
+
lines.push(` tokenUrl: ${JSON.stringify(tokenUrl)},`);
|
|
280
|
+
lines.push(` scopes: ${JSON.stringify(scopes)},`);
|
|
181
281
|
lines.push('}');
|
|
182
282
|
lines.push('');
|
|
183
283
|
}
|
|
@@ -185,19 +285,29 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
185
285
|
const routes = {};
|
|
186
286
|
for (const { parsed } of opPairs) {
|
|
187
287
|
const key = `${parsed.method.toUpperCase()} ${parsed.path}`;
|
|
188
|
-
|
|
288
|
+
const route = {
|
|
189
289
|
path: parsed.pathParams.map((p) => p.name),
|
|
190
290
|
query: parsed.queryParams.map((p) => p.name),
|
|
291
|
+
headers: parsed.headerParams.map((p) => p.name),
|
|
191
292
|
};
|
|
293
|
+
if (parsed.errorResponses.length > 0) {
|
|
294
|
+
route.errors = {};
|
|
295
|
+
for (const err of parsed.errorResponses) {
|
|
296
|
+
route.errors[err.statusCode] = err.description;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
routes[key] = route;
|
|
192
300
|
}
|
|
193
|
-
lines.push(`const ROUTES: Record<string, { path: string[], query: string[] }> = ${JSON.stringify(routes, null, 2)}`);
|
|
301
|
+
lines.push(`const ROUTES: Record<string, { path: string[], query: string[], headers: string[], errors?: Record<number, string> }> = ${JSON.stringify(routes, null, 2)}`);
|
|
194
302
|
lines.push('');
|
|
195
303
|
// Class declaration
|
|
196
304
|
lines.push(`export class ${pascalName}Service {`);
|
|
305
|
+
lines.push(' private baseUrl: string');
|
|
197
306
|
if (flags.oauth) {
|
|
198
307
|
lines.push(' private oauth: OAuth2Client');
|
|
199
308
|
lines.push('');
|
|
200
|
-
lines.push(` constructor(secrets: TypedSecretService) {`);
|
|
309
|
+
lines.push(` constructor(secrets: TypedSecretService, variables: TypedVariablesService) {`);
|
|
310
|
+
lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
|
|
201
311
|
lines.push(' this.oauth = new OAuth2Client(');
|
|
202
312
|
lines.push(` ${screamingName}_OAUTH2_CONFIG,`);
|
|
203
313
|
lines.push(` '${screamingName}_APP_CREDENTIALS',`);
|
|
@@ -206,10 +316,19 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
206
316
|
lines.push(' }');
|
|
207
317
|
}
|
|
208
318
|
else if (flags.secret) {
|
|
209
|
-
lines.push(
|
|
319
|
+
lines.push('');
|
|
320
|
+
lines.push(` constructor(private creds: ${pascalName}Secrets, variables: TypedVariablesService) {`);
|
|
321
|
+
lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
|
|
322
|
+
lines.push(' }');
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
lines.push('');
|
|
326
|
+
lines.push(` constructor(variables: TypedVariablesService) {`);
|
|
327
|
+
lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
|
|
328
|
+
lines.push(' }');
|
|
210
329
|
}
|
|
211
330
|
lines.push('');
|
|
212
|
-
// call() method — splits data into path/query/body using route map
|
|
331
|
+
// call() method — splits data into path/query/headers/body using route map
|
|
213
332
|
lines.push(' async call<T>(');
|
|
214
333
|
lines.push(" method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',");
|
|
215
334
|
lines.push(' path: string,');
|
|
@@ -219,6 +338,9 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
219
338
|
lines.push(' let endpoint = path');
|
|
220
339
|
lines.push(' let body: Record<string, unknown> | undefined');
|
|
221
340
|
lines.push(' const query: Record<string, string> = {}');
|
|
341
|
+
lines.push(' const headers: Record<string, string> = {');
|
|
342
|
+
lines.push(" 'Content-Type': 'application/json',");
|
|
343
|
+
lines.push(' }');
|
|
222
344
|
lines.push('');
|
|
223
345
|
lines.push(' if (data && route) {');
|
|
224
346
|
lines.push(' // Interpolate path params');
|
|
@@ -233,17 +355,23 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
233
355
|
lines.push(' query[param] = String(data[param])');
|
|
234
356
|
lines.push(' }');
|
|
235
357
|
lines.push(' }');
|
|
358
|
+
lines.push(' // Extract header params');
|
|
359
|
+
lines.push(' for (const param of route.headers) {');
|
|
360
|
+
lines.push(' if (data[param] !== undefined) {');
|
|
361
|
+
lines.push(' headers[param] = String(data[param])');
|
|
362
|
+
lines.push(' }');
|
|
363
|
+
lines.push(' }');
|
|
236
364
|
lines.push(' // Everything else goes into body');
|
|
237
|
-
lines.push(' const
|
|
365
|
+
lines.push(' const pathQueryHeaders = new Set([...route.path, ...route.query, ...route.headers])');
|
|
238
366
|
lines.push(' const remaining = Object.fromEntries(');
|
|
239
|
-
lines.push(' Object.entries(data).filter(([k]) => !
|
|
367
|
+
lines.push(' Object.entries(data).filter(([k]) => !pathQueryHeaders.has(k))');
|
|
240
368
|
lines.push(' )');
|
|
241
369
|
lines.push(' if (Object.keys(remaining).length > 0) {');
|
|
242
370
|
lines.push(' body = remaining');
|
|
243
371
|
lines.push(' }');
|
|
244
372
|
lines.push(' }');
|
|
245
373
|
lines.push('');
|
|
246
|
-
lines.push(' const url = new URL(`${
|
|
374
|
+
lines.push(' const url = new URL(`${this.baseUrl}${endpoint}`)');
|
|
247
375
|
lines.push(' for (const [key, value] of Object.entries(query)) {');
|
|
248
376
|
lines.push(' url.searchParams.set(key, value)');
|
|
249
377
|
lines.push(' }');
|
|
@@ -251,38 +379,78 @@ function generateServiceFile(spec, opPairs, vars, flags) {
|
|
|
251
379
|
if (flags.oauth) {
|
|
252
380
|
lines.push(' const response = await this.oauth.request(url.toString(), {');
|
|
253
381
|
lines.push(' method,');
|
|
254
|
-
lines.push(
|
|
382
|
+
lines.push(' headers,');
|
|
255
383
|
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
256
384
|
lines.push(' })');
|
|
257
385
|
}
|
|
258
386
|
else if (flags.secret) {
|
|
387
|
+
// Use apiKey details from spec if available
|
|
388
|
+
const apiKeyScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'apiKey');
|
|
389
|
+
if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
|
|
390
|
+
lines.push(` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`');
|
|
394
|
+
}
|
|
395
|
+
lines.push('');
|
|
259
396
|
lines.push(' const response = await fetch(url.toString(), {');
|
|
260
397
|
lines.push(' method,');
|
|
261
|
-
lines.push(' headers
|
|
262
|
-
lines.push(" 'Content-Type': 'application/json',");
|
|
263
|
-
lines.push(' Authorization: `Bearer ${this.creds.apiKey}`,');
|
|
264
|
-
lines.push(' },');
|
|
398
|
+
lines.push(' headers,');
|
|
265
399
|
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
266
400
|
lines.push(' })');
|
|
267
401
|
}
|
|
268
402
|
else {
|
|
269
403
|
lines.push(' const response = await fetch(url.toString(), {');
|
|
270
404
|
lines.push(' method,');
|
|
271
|
-
lines.push(
|
|
405
|
+
lines.push(' headers,');
|
|
272
406
|
lines.push(' body: body ? JSON.stringify(body) : undefined,');
|
|
273
407
|
lines.push(' })');
|
|
274
408
|
}
|
|
275
409
|
lines.push('');
|
|
276
410
|
lines.push(' if (!response.ok) {');
|
|
277
411
|
lines.push(' const errorText = await response.text()');
|
|
278
|
-
lines.push(
|
|
412
|
+
lines.push(' const errorMessage = route?.errors?.[response.status] ?? errorText');
|
|
413
|
+
lines.push(' switch (response.status) {');
|
|
414
|
+
lines.push(' case 400: throw new BadRequestError(errorMessage)');
|
|
415
|
+
lines.push(' case 401: throw new UnauthorizedError(errorMessage)');
|
|
416
|
+
lines.push(' case 403: throw new ForbiddenError(errorMessage)');
|
|
417
|
+
lines.push(' case 404: throw new NotFoundError(errorMessage)');
|
|
418
|
+
lines.push(' case 405: throw new MethodNotAllowedError(errorMessage)');
|
|
419
|
+
lines.push(' case 409: throw new ConflictError(errorMessage)');
|
|
420
|
+
lines.push(' case 422: throw new UnprocessableContentError(errorMessage)');
|
|
421
|
+
lines.push(' case 429: throw new TooManyRequestsError(errorMessage)');
|
|
422
|
+
lines.push(' case 500: throw new InternalServerError(errorMessage)');
|
|
423
|
+
lines.push(` default: throw new Error(\`${displayName} API error (\${response.status}): \${errorText}\`)`);
|
|
424
|
+
lines.push(' }');
|
|
279
425
|
lines.push(' }');
|
|
280
426
|
lines.push('');
|
|
281
427
|
lines.push(' const text = await response.text()');
|
|
282
|
-
lines.push(
|
|
428
|
+
lines.push(' if (!text) return {} as T');
|
|
283
429
|
lines.push(' return JSON.parse(text) as T');
|
|
284
430
|
lines.push(' }');
|
|
285
431
|
lines.push('}');
|
|
286
432
|
lines.push('');
|
|
287
433
|
return lines.join('\n');
|
|
288
434
|
}
|
|
435
|
+
function generateVariableFile(spec, vars) {
|
|
436
|
+
const { camelName, screamingName, displayName } = vars;
|
|
437
|
+
const serverUrls = spec.serverUrls.length > 0 ? spec.serverUrls : [];
|
|
438
|
+
const defaultUrl = serverUrls[0];
|
|
439
|
+
const lines = [];
|
|
440
|
+
lines.push("import { z } from 'zod'");
|
|
441
|
+
lines.push("import { wireVariable } from '@pikku/core/variable'");
|
|
442
|
+
lines.push('');
|
|
443
|
+
const schemaVarName = `${camelName}BaseUrlSchema`;
|
|
444
|
+
const urlsLiteral = serverUrls.map((u) => JSON.stringify(u)).join(', ');
|
|
445
|
+
lines.push(`export const ${schemaVarName} = z.enum([${urlsLiteral}]).default(${JSON.stringify(defaultUrl)})`);
|
|
446
|
+
lines.push('');
|
|
447
|
+
lines.push(`wireVariable({`);
|
|
448
|
+
lines.push(` name: '${screamingName}_BASE_URL',`);
|
|
449
|
+
lines.push(` displayName: '${displayName} Base URL',`);
|
|
450
|
+
lines.push(` description: 'The base URL for the ${displayName} API.',`);
|
|
451
|
+
lines.push(` variableId: '${screamingName}_BASE_URL',`);
|
|
452
|
+
lines.push(` schema: ${schemaVarName},`);
|
|
453
|
+
lines.push(`})`);
|
|
454
|
+
lines.push('');
|
|
455
|
+
return lines.join('\n');
|
|
456
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import type { OpenAPISchema } from './zod-codegen.js';
|
|
2
|
+
export interface ErrorResponse {
|
|
3
|
+
statusCode: number;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export interface SecuritySchemeInfo {
|
|
7
|
+
type: 'oauth2' | 'http' | 'apiKey';
|
|
8
|
+
scheme?: string;
|
|
9
|
+
bearerFormat?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
in?: string;
|
|
12
|
+
flows?: {
|
|
13
|
+
authorizationUrl?: string;
|
|
14
|
+
tokenUrl?: string;
|
|
15
|
+
scopes?: Record<string, string>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
2
18
|
export interface ParsedSpec {
|
|
3
19
|
info: {
|
|
4
20
|
title: string;
|
|
@@ -6,9 +22,12 @@ export interface ParsedSpec {
|
|
|
6
22
|
description?: string;
|
|
7
23
|
};
|
|
8
24
|
baseUrl: string;
|
|
25
|
+
serverUrls: string[];
|
|
9
26
|
authType: 'bearer' | 'oauth2' | 'apiKey' | 'none';
|
|
10
27
|
operations: ParsedOperation[];
|
|
11
28
|
componentSchemas: Record<string, OpenAPISchema>;
|
|
29
|
+
securitySchemes: Record<string, SecuritySchemeInfo>;
|
|
30
|
+
tagDescriptions: Record<string, string>;
|
|
12
31
|
}
|
|
13
32
|
export interface ParsedOperation {
|
|
14
33
|
operationId?: string;
|
|
@@ -19,15 +38,21 @@ export interface ParsedOperation {
|
|
|
19
38
|
tags: string[];
|
|
20
39
|
pathParams: ParsedParam[];
|
|
21
40
|
queryParams: ParsedParam[];
|
|
41
|
+
headerParams: ParsedParam[];
|
|
22
42
|
requestBody?: OpenAPISchema;
|
|
43
|
+
requestBodyDescription?: string;
|
|
44
|
+
requestBodyRequired?: boolean;
|
|
23
45
|
responseSchema?: OpenAPISchema;
|
|
24
46
|
responseDescription?: string;
|
|
47
|
+
errorResponses: ErrorResponse[];
|
|
48
|
+
deprecated: boolean;
|
|
25
49
|
}
|
|
26
50
|
export interface ParsedParam {
|
|
27
51
|
name: string;
|
|
28
52
|
required: boolean;
|
|
29
53
|
schema: OpenAPISchema;
|
|
30
54
|
description?: string;
|
|
55
|
+
example?: unknown;
|
|
31
56
|
}
|
|
32
57
|
/**
|
|
33
58
|
* Read and parse an OpenAPI spec from a file path.
|