Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
244b49f
feat: Auto-configure security from OpenAPI securitySchemes
jonaslagoni Mar 8, 2026
1a6014c
fix: use dynamic port assignment in HTTP runtime tests
jonaslagoni Mar 8, 2026
dec8c7b
fix: ensure OAuth2 helper functions are always available in generated…
jonaslagoni Mar 9, 2026
bdba1cb
wip
jonaslagoni Mar 9, 2026
7274f5a
fix: address PR review comments for HTTP security types
jonaslagoni Mar 9, 2026
6ef33d1
fix: escape OpenAPI spec values in generated TypeScript code
jonaslagoni Mar 9, 2026
8663c47
refactor: extract shared API key defaults logic into helper function
jonaslagoni Mar 9, 2026
0118d21
fix: update tests for API key defaults from spec
jonaslagoni Mar 9, 2026
20fceee
fix: resolve ESLint errors in fetch.ts
jonaslagoni Mar 9, 2026
8e56847
Merge main into issue-337-autoconfigure-security-from-op
jonaslagoni Mar 9, 2026
a8a30cb
Merge main into issue-337-autoconfigure-security-from-op
jonaslagoni Mar 9, 2026
a22a20b
Merge main into issue-337-autoconfigure-security-from-op
jonaslagoni Mar 12, 2026
6d80cef
refactor: rename ExtractedSecurityScheme to SecuritySchemeOptions
jonaslagoni Mar 12, 2026
ac131b0
Merge main into issue-337-autoconfigure-security-from-op
jonaslagoni Mar 12, 2026
1292015
fix: resolve PR review comments on security scheme handling
jonaslagoni Mar 12, 2026
bc3fa7d
fix: generate all auth types when empty security schemes array provided
jonaslagoni Mar 12, 2026
39a3d2c
fix: handle Node.js deprecation warnings in init tests
jonaslagoni Mar 12, 2026
184235c
fix: add curly braces to satisfy ESLint curly rule
jonaslagoni Mar 12, 2026
840f850
wip
jonaslagoni Mar 12, 2026
a8084d7
fix: escape OAuth2 scope names in generated JSDoc comments
jonaslagoni Mar 12, 2026
3e525d8
rework fetch
jonaslagoni Mar 12, 2026
2cc39f0
refactor code
jonaslagoni Mar 12, 2026
5d997f3
fix: add newline and carriage return escaping to escapeStringForCodeGen
jonaslagoni Mar 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/codegen/generators/typescript/channels/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ import {collectProtocolDependencies} from './utils';
import {
renderHttpFetchClient,
renderHttpCommonTypes
} from './protocols/http/fetch';
} from './protocols/http';
import {getMessageTypeAndModule} from './utils';
import {pascalCase} from '../utils';
import {createMissingInputDocumentError} from '../../../errors';
import {resolveImportExtension} from '../../../utils';
import {extractSecuritySchemes} from '../../../inputs/openapi/security';

type OpenAPIDocument =
| OpenAPIV3.Document
Expand Down Expand Up @@ -75,6 +76,9 @@ export async function generateTypeScriptChannelsForOpenAPI(

const {openapiDocument} = validateOpenAPIContext(context);

// Extract security schemes from the OpenAPI document
const securitySchemes = extractSecuritySchemes(openapiDocument);

// Collect dependencies
const deps = protocolDependencies['http_client'];
const importExtension = resolveImportExtension(
Expand All @@ -98,8 +102,9 @@ export async function generateTypeScriptChannelsForOpenAPI(
);

// Generate common types once (stateless check)
// Pass security schemes to generate only relevant auth types
if (protocolCodeFunctions['http_client'].length === 0 && renders.length > 0) {
const commonTypesCode = renderHttpCommonTypes();
const commonTypesCode = renderHttpCommonTypes(securitySchemes);
protocolCodeFunctions['http_client'].unshift(commonTypesCode);
}

Expand Down
285 changes: 285 additions & 0 deletions src/codegen/generators/typescript/channels/protocols/http/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
/**
* Generates HTTP client functions for individual API operations.
* Each operation gets a typed function with request/response handling.
*/
import {HttpRenderType} from '../../../../../types';
import {pascalCase} from '../../../utils';
import {ChannelFunctionTypes, RenderHttpParameters} from '../../types';

/**
* Renders an HTTP fetch client function for a specific API operation.
*/
export function renderHttpFetchClient({
requestTopic,
requestMessageType,
requestMessageModule,
replyMessageType,
replyMessageModule,
channelParameters,
method,
servers = [],
subName = pascalCase(requestTopic),
functionName = `${method.toLowerCase()}${subName}`,
includesStatusCodes = false
}: RenderHttpParameters): HttpRenderType {
const messageType = requestMessageModule
? `${requestMessageModule}.${requestMessageType}`
: requestMessageType;
const replyType = replyMessageModule
? `${replyMessageModule}.${replyMessageType}`
: replyMessageType;

// Generate context interface name
const contextInterfaceName = `${pascalCase(functionName)}Context`;

// Determine if operation has path parameters
const hasParameters = channelParameters !== undefined;

// Generate the context interface (extends HttpClientContext)
const contextInterface = generateContextInterface(
contextInterfaceName,
messageType,
hasParameters,
method
);

// Generate the function implementation
const functionCode = generateFunctionImplementation({
functionName,
contextInterfaceName,
replyType,
replyMessageModule,
replyMessageType,
messageType,
requestTopic,
hasParameters,
method,
servers,
includesStatusCodes
});

const code = `${contextInterface}

${functionCode}`;

return {
messageType,
replyType,
code,
functionName,
dependencies: [
`import { URLSearchParams, URL } from 'url';`,
`import * as NodeFetch from 'node-fetch';`
],
functionType: ChannelFunctionTypes.HTTP_CLIENT
};
}

/**
* Generate the context interface for an HTTP operation
*/
function generateContextInterface(
interfaceName: string,
messageType: string | undefined,
hasParameters: boolean,
method: string
): string {
const fields: string[] = [];

// Add payload field for methods that have a body
if (messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
fields.push(` payload: ${messageType};`);
}

// Add parameters field if operation has path parameters
if (hasParameters) {
fields.push(
` parameters: { getChannelWithParameters: (path: string) => string };`
);
}

// Add requestHeaders field (optional) for operations that support typed headers
// This is always optional since headers can also be passed via additionalHeaders
fields.push(` requestHeaders?: { marshal: () => string };`);

const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : '';

return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`;
}

/**
* Generate the function implementation
*/
function generateFunctionImplementation(params: {
functionName: string;
contextInterfaceName: string;
replyType: string;
replyMessageModule: string | undefined;
replyMessageType: string;
messageType: string | undefined;
requestTopic: string;
hasParameters: boolean;
method: string;
servers: string[];
includesStatusCodes: boolean;
}): string {
const {
functionName,
contextInterfaceName,
replyType,
replyMessageModule,
replyMessageType,
messageType,
requestTopic,
hasParameters,
method,
servers,
includesStatusCodes
} = params;

const defaultServer = servers[0] ?? "'localhost:3000'";
const hasBody =
messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase());

// Generate URL building code
const urlBuildCode = hasParameters
? `let url = buildUrlWithParameters(config.server, '${requestTopic}', context.parameters);`
: 'let url = `${config.server}${config.path}`;';

// Generate headers initialization
const headersInit = `let headers = context.requestHeaders
? applyTypedHeaders(context.requestHeaders, config.additionalHeaders)
: { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record<string, string | string[]>;`;

// Generate body preparation
const bodyPrep = hasBody
? `const body = context.payload?.marshal();`
: `const body = undefined;`;

// Generate response parsing
// Use unmarshalByStatusCode if the payload is a union type with status code support
let responseParseCode: string;
if (replyMessageModule) {
responseParseCode = includesStatusCodes
? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(rawData, response.status);`
: `const responseData = ${replyMessageModule}.unmarshal(rawData);`;
} else {
responseParseCode = `const responseData = ${replyMessageType}.unmarshal(rawData);`;
}

// Generate default context for optional context parameter
const contextDefault = !hasBody && !hasParameters ? ' = {}' : '';

return `async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise<HttpClientResponse<${replyType}>> {
// Apply defaults
const config = {
path: '${requestTopic}',
server: ${defaultServer},
...context,
};

// Validate OAuth2 config if present
if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
validateOAuth2Config(config.auth);
}

// Build headers
${headersInit}

// Build URL
${urlBuildCode}
url = applyQueryParams(config.queryParams, url);

// Apply pagination (can affect URL and/or headers)
const paginationResult = applyPagination(config.pagination, url, headers);
url = paginationResult.url;
headers = paginationResult.headers;

// Apply authentication
const authResult = applyAuth(config.auth, headers, url);
headers = authResult.headers;
url = authResult.url;

// Prepare body
${bodyPrep}

// Determine request function
const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest;

// Build request params
let requestParams: HttpRequestParams = {
url,
method: '${method}',
headers,
body
};

// Apply beforeRequest hook
if (config.hooks?.beforeRequest) {
requestParams = await config.hooks.beforeRequest(requestParams);
}

try {
// Execute request with retry logic
let response = await executeWithRetry(requestParams, makeRequest, config.retry);

// Apply afterResponse hook
if (config.hooks?.afterResponse) {
response = await config.hooks.afterResponse(response, requestParams);
}

// Handle OAuth2 token flows that require getting a token first
if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) {
const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry);
if (tokenFlowResponse) {
response = tokenFlowResponse;
}
}

// Handle 401 with token refresh
if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
try {
const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry);
if (refreshResponse) {
response = refreshResponse;
}
} catch {
throw new Error('Unauthorized');
}
}

// Handle error responses
if (!response.ok) {
handleHttpError(response.status, response.statusText);
}

// Parse response
const rawData = await response.json();
${responseParseCode}

// Extract response metadata
const responseHeaders = extractHeaders(response);
const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination);

// Build response wrapper with pagination helpers
const result: HttpClientResponse<${replyType}> = {
data: responseData,
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
rawData,
pagination: paginationInfo,
...createPaginationHelpers(config, paginationInfo, ${functionName}),
};

return result;

} catch (error) {
// Apply onError hook if present
if (config.hooks?.onError && error instanceof Error) {
throw await config.hooks.onError(error, requestParams);
}
throw error;
}
}`;
}
Loading
Loading