diff --git a/docs/generators/client.md b/docs/generators/client.md index 2401cc57..48bb78c0 100644 --- a/docs/generators/client.md +++ b/docs/generators/client.md @@ -116,7 +116,7 @@ export class NatsClient { ### HTTP -For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`server`, `auth`, `hooks`, retry, pagination, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. +For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`baseUrl`, `auth`, `hooks`, retry, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. ```js export default { @@ -140,7 +140,7 @@ Example generated usage: import {SafepayNordicClient} from './__gen__/client/SafepayNordicClient'; const safepay = new SafepayNordicClient({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: {type: 'bearer', token: process.env.API_TOKEN ?? ''} }); @@ -149,4 +149,4 @@ const response = await safepay.getV2Documents(); console.log(response.status, response.data); ``` -Each method returns the same `HttpClientResponse` as the underlying channel function, so response metadata (status, headers, pagination helpers) is preserved. \ No newline at end of file +Each method returns the same `HttpClientResponse` as the underlying channel function, so response metadata (status, headers) is preserved. \ No newline at end of file diff --git a/docs/protocols/http_client.md b/docs/protocols/http_client.md index 4b942efc..236c67e3 100644 --- a/docs/protocols/http_client.md +++ b/docs/protocols/http_client.md @@ -4,7 +4,7 @@ sidebar_position: 99 # HTTP(S) -HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, pagination, retry logic, and extensibility hooks. +HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, retry logic, and extensibility hooks. It is currently available through the generators ([channels](../generators/channels.md)): @@ -16,10 +16,6 @@ This is available through [AsyncAPI](../inputs/asyncapi.md) ([requires the HTTP |---|---| | Download | ❌ | | Upload | ❌ | -| Offset based Pagination | ✅ | -| Cursor based Pagination | ✅ | -| Page based Pagination | ✅ | -| Range based Pagination | ✅ | | Retry with backoff | ✅ | | OAuth2 Authorization code | ❌ (browser-only) | | OAuth2 Implicit | ❌ (browser-only) | @@ -118,7 +114,7 @@ const pingMessage = new Ping({ message: 'Hello!' }); // Make a simple request const response = await postPingPostRequest({ payload: pingMessage, - server: 'https://api.example.com' + baseUrl: 'https://api.example.com' }); // Access the response @@ -147,14 +143,14 @@ import { GetV2ConnectReferenceIdParameters } from './__gen__/channels/parameter/ // Request with a body: build the model, pass it as `payload`. const created = await http_client.postV2Connect({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', payload: new PostV2ConnectRequest({ returnUrl: 'https://shop.example/return' }) }); console.log(created.data.connectUrl); // typed response model // Request with a path parameter: supply it through the parameter model. const connect = await http_client.getV2ConnectReferenceId({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: new GetV2ConnectReferenceIdParameters({ referenceId: 'ref_123' }) }); console.log(connect.data.safepayAccountId); @@ -171,7 +167,7 @@ The HTTP client uses a discriminated union for authentication, providing excelle ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'bearer', token: 'your-jwt-token' @@ -184,7 +180,7 @@ const response = await postPingPostRequest({ ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'basic', username: 'user', @@ -199,7 +195,7 @@ const response = await postPingPostRequest({ // API Key in header (default) const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'apiKey', key: 'your-api-key', @@ -211,7 +207,7 @@ const response = await postPingPostRequest({ // API Key in query parameter const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'apiKey', key: 'your-api-key', @@ -228,7 +224,7 @@ For server-to-server authentication: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', flow: 'client_credentials', @@ -251,7 +247,7 @@ For legacy applications requiring username/password: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', flow: 'password', @@ -277,7 +273,7 @@ const accessToken = getTokenFromBrowserFlow(); const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', accessToken: accessToken, @@ -292,81 +288,6 @@ const response = await postPingPostRequest({ }); ``` -## Pagination - -The HTTP client supports multiple pagination strategies. Pagination parameters can be placed in query parameters or headers. - -### Offset-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'offset', - offset: 0, - limit: 25, - in: 'query', // 'query' or 'header' - offsetParam: 'offset', // Query param name (default: 'offset') - limitParam: 'limit' // Query param name (default: 'limit') - } -}); - -// Navigate pages -if (response.hasNextPage?.()) { - const nextPage = await response.getNextPage?.(); -} -``` - -### Cursor-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'cursor', - cursor: undefined, // First page - limit: 25, - cursorParam: 'cursor' - } -}); - -// Get next page using cursor from response -if (response.pagination?.nextCursor) { - const nextPage = await response.getNextPage?.(); -} -``` - -### Page-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'page', - page: 1, - pageSize: 25, - pageParam: 'page', - pageSizeParam: 'per_page' - } -}); -``` - -### Range-based Pagination (RFC 7233) - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'range', - start: 0, - end: 24, - unit: 'items', // Range unit (default: 'items') - rangeHeader: 'Range' // Header name (default: 'Range') - } -}); -// Sends: Range: items=0-24 -``` - ## Retry with Exponential Backoff Configure automatic retry for failed requests: @@ -374,7 +295,7 @@ Configure automatic retry for failed requests: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', retry: { maxRetries: 3, // Maximum retry attempts (default: 3) initialDelayMs: 1000, // Initial delay before first retry (default: 1000) @@ -396,7 +317,7 @@ Customize request behavior with hooks: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', hooks: { // Modify request before sending beforeRequest: async (params) => { @@ -459,7 +380,7 @@ const params = new UserItemsParameters({ }); const response = await getGetUserItem({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params // Replaces {userId} and {itemId} in path }); ``` @@ -477,7 +398,7 @@ const headers = new ItemRequestHeaders({ }); const response = await putUpdateUserItem({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params, payload: itemData, requestHeaders: headers // Type-safe headers @@ -491,12 +412,12 @@ Add custom headers or query parameters to any request: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', additionalHeaders: { 'X-Custom-Header': 'value', 'Accept-Language': 'en-US' }, - queryParams: { + additionalQueryParams: { include: 'metadata', format: 'detailed' } @@ -519,7 +440,7 @@ operations: ```typescript const response = await getItemRequest({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params }); diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index 98eb722c..485cac73 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -106,6 +106,7 @@ export async function generateTypeScriptChannelsForOpenAPI( openapiDocument, payloads, parameters, + headers, oauth2Enabled ); @@ -136,6 +137,7 @@ function processOpenAPIOperations( openapiDocument: OpenAPIDocument, payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, + headers: TypeScriptHeadersRenderType, oauth2Enabled: boolean ): ReturnType[] { const renders: ReturnType[] = []; @@ -152,6 +154,7 @@ function processOpenAPIOperations( path, payloads, parameters, + headers, oauth2Enabled ); if (render) { @@ -172,6 +175,7 @@ function processOperation( path: string, payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, + headers: TypeScriptHeadersRenderType, oauth2Enabled: boolean ): ReturnType | undefined { // eslint-disable-next-line security/detect-object-injection @@ -202,6 +206,10 @@ function processOperation( // eslint-disable-next-line security/detect-object-injection const parameterModel = parameters.channelModels[operationId]; + // Look up headers + // eslint-disable-next-line security/detect-object-injection + const headersModel = headers.channelModels[operationId]; + // Get message types - handle undefined payloads const requestMessageInfo = requestPayload ? getMessageTypeAndModule(requestPayload) @@ -257,11 +265,13 @@ function processOperation( channelParameters: parameterModel?.model as | ConstrainedObjectModel | undefined, + channelHeaders: headersModel?.model as ConstrainedObjectModel | undefined, includesStatusCodes: replyIncludesStatusCodes, description, deprecated, oauth2Enabled, - hasSerializeUrl: parameterModel !== undefined + hasSerializeUrl: parameterModel !== undefined, + hasSerializeHeaders: headersModel !== undefined }); // Grouping metadata for the `organization` option (consumed in diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 98d43c74..96cd80f4 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -26,7 +26,8 @@ export function renderHttpFetchClient({ description, deprecated, oauth2Enabled = true, - hasSerializeUrl = false + hasSerializeUrl = false, + hasSerializeHeaders = false }: RenderHttpParameters): HttpRenderType { const messageType = requestMessageModule ? `${requestMessageModule}.${requestMessageType}` @@ -38,8 +39,9 @@ export function renderHttpFetchClient({ // Generate context interface name const contextInterfaceName = `${pascalCase(functionName)}Context`; - // Determine if operation has path parameters + // Determine if operation has path parameters or headers const hasParameters = channelParameters !== undefined; + const hasHeaders = channelHeaders !== undefined; // Generate the context interface (extends HttpClientContext) const contextInterface = generateContextInterface( @@ -67,6 +69,9 @@ export function renderHttpFetchClient({ messageType, requestTopic, hasParameters, + hasHeaders, + headersType: channelHeaders?.type, + hasSerializeHeaders, parametersType: channelParameters?.type, method, servers, @@ -114,12 +119,11 @@ function generateContextInterface( fields.push(` parameters: ${parametersType};`); } - // Reference the concrete generated headers model when available; fall back to - // the structural marshal contract otherwise. Always optional since headers can - // also be passed via additionalHeaders. - fields.push( - ` requestHeaders?: ${headersType ?? '{ marshal: () => string }'};` - ); + // Emit requestHeaders only when the spec defines operation headers so the + // context stays minimal for operations that don't have them. + if (headersType) { + fields.push(` requestHeaders?: ${headersType};`); + } const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : ''; @@ -133,12 +137,12 @@ function buildUrlCode( hasSerializeUrl: boolean ): string { if (!hasParameters || !parametersType) { - return 'let url = `${config.server}${config.path}`;'; + return `let url = \`\${config.baseUrl}${requestTopic}\`;`; } const serializeFn = hasSerializeUrl ? `serialize${parametersType}Url(context.parameters, path)` : `context.parameters.getChannelWithParameters(path)`; - return `let url = buildUrlWithParameters(config.server, '${requestTopic}', (path) => ${serializeFn});`; + return `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', (path) => ${serializeFn});`; } /** @@ -153,6 +157,9 @@ function generateFunctionImplementation(params: { messageType: string | undefined; requestTopic: string; hasParameters: boolean; + hasHeaders: boolean; + headersType: string | undefined; + hasSerializeHeaders: boolean; parametersType: string | undefined; method: string; servers: string[]; @@ -170,6 +177,9 @@ function generateFunctionImplementation(params: { messageType, requestTopic, hasParameters, + hasHeaders, + headersType, + hasSerializeHeaders, parametersType, method, servers, @@ -179,16 +189,23 @@ function generateFunctionImplementation(params: { hasSerializeUrl } = params; - const defaultServer = servers[0] ?? "'localhost:3000'"; + const defaultServer = servers[0] ?? "'http://localhost:3000'"; const hasBody = messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); const urlBuildCode = buildUrlCode(requestTopic, hasParameters, parametersType, hasSerializeUrl); // Generate headers initialization - const headersInit = `let headers = context.requestHeaders + let headersInit: string; + if (!hasHeaders) { + headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + } else if (hasSerializeHeaders) { + headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serialize${headersType}Headers(context.requestHeaders) : {}) } as Record;`; + } else { + headersInit = `let headers = context.requestHeaders ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + } // Generate body preparation const bodyPrep = hasBody @@ -254,8 +271,7 @@ function generateFunctionImplementation(params: { async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { // Apply defaults const config = { - path: '${requestTopic}', - server: ${defaultServer}, + baseUrl: ${defaultServer}, ...context, }; @@ -264,12 +280,7 @@ ${oauth2ValidateBlock} // Build headers // 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; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -315,17 +326,13 @@ ${oauth2TokenBlock} // 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; diff --git a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts index 3f48118b..2fcb6371 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -70,26 +70,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -104,16 +84,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -138,69 +108,6 @@ export interface TokenResponse { ${securityTypes} ${authFeaturesBlock}${apiKeyDefaultsBlock} -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -258,15 +165,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -276,8 +179,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -337,94 +240,6 @@ ${applyAuthCases} return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -567,199 +382,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index a94e7740..0eb4bc3b 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -310,6 +310,12 @@ export interface RenderHttpParameters { * `getChannelWithParameters` method (AsyncAPI style). Defaults to false. */ hasSerializeUrl?: boolean; + /** + * Whether the header model exports a standalone `serializeXHeaders` function + * (OpenAPI plain-object style). When false, falls back to the class-based + * `.marshal()` method (AsyncAPI style). Defaults to false. + */ + hasSerializeHeaders?: boolean; } export type SupportedProtocols = diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index dd95c484..e44c3d1a 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -121,7 +121,8 @@ export function addHeadersToDependencies( headerGenerator: {outputPath: string}, currentGenerator: {outputPath: string}, dependencies: string[], - importExtension: ImportExtension = 'none' + importExtension: ImportExtension = 'none', + headerFunctions?: Record ) { Object.values(headers) .filter((model) => model !== undefined) @@ -138,7 +139,12 @@ export function addHeadersToDependencies( importExtension ); - dependencies.push(`import {${header.modelName}} from '${importPath}';`); + const fns = headerFunctions?.[header.modelName] ?? []; + const importNames = + fns.length > 0 + ? `${header.modelName}, ${fns.join(', ')}` + : header.modelName; + dependencies.push(`import {${importNames}} from '${importPath}';`); }); } export function getMessageTypeAndModule(payload: ChannelPayload) { @@ -246,7 +252,8 @@ export function collectProtocolDependencies( headers.generator, context.generator, protocolDeps, - importExtension + importExtension, + headers.headerFunctions ); } } diff --git a/src/codegen/generators/typescript/client/protocols/http.ts b/src/codegen/generators/typescript/client/protocols/http.ts index 0eedb80c..02ec46e3 100644 --- a/src/codegen/generators/typescript/client/protocols/http.ts +++ b/src/codegen/generators/typescript/client/protocols/http.ts @@ -178,7 +178,7 @@ import * as http_client from '${channelImportPath}'; * @class ${className} * * ${classDescription} Construct it once with the shared request configuration - * (server, auth, hooks, ...) and call the operation methods; every method + * (baseUrl, auth, hooks, ...) and call the operation methods; every method * forwards to the underlying channel function with that configuration applied. */ export class ${className} { diff --git a/src/codegen/generators/typescript/headers.ts b/src/codegen/generators/typescript/headers.ts index fd0d1cd0..4b234216 100644 --- a/src/codegen/generators/typescript/headers.ts +++ b/src/codegen/generators/typescript/headers.ts @@ -10,7 +10,12 @@ import {z} from 'zod'; import {defaultCodegenTypescriptModelinaOptions} from './utils'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {processAsyncAPIHeaders} from '../../inputs/asyncapi/generators/headers'; -import {processOpenAPIHeaders} from '../../inputs/openapi/generators/headers'; +import { + processOpenAPIHeaders, + createOpenAPIHeadersGenerator, + generateOpenAPIHeaderFunctions +} from '../../inputs/openapi/generators/headers'; +import {ConstrainedObjectModel} from '@asyncapi/modelina'; import { TS_DESCRIPTION_PRESET, TS_COMMON_PRESET, @@ -99,20 +104,11 @@ export interface ProcessedHeadersData { >; } -// Core generator function that works with processed data -export async function generateTypescriptHeadersCore({ - processedData, - context -}: { - processedData: ProcessedHeadersData; - context: TypescriptHeadersContext; -}): Promise<{ - channelModels: Record; - files: GeneratedFile[]; -}> { - const {generator} = context; - - const modelinaGenerator = new TypeScriptFileGenerator({ +function createAsyncAPIHeadersGenerator( + generator: TypescriptHeadersGeneratorInternal, + context: TypescriptHeadersContext +): TypeScriptFileGenerator { + return new TypeScriptFileGenerator({ ...defaultCodegenTypescriptModelinaOptions, constraints: { propertyKey: typeScriptDefaultPropertyKeyConstraints({ @@ -123,52 +119,95 @@ export async function generateTypescriptHeadersCore({ useJavascriptReservedKeywords: false, presets: [ TS_DESCRIPTION_PRESET, - { - preset: TS_COMMON_PRESET, - options: { - marshalling: true - } - }, - createValidationPreset( - { - includeValidation: generator.includeValidation - }, - context - ) + {preset: TS_COMMON_PRESET, options: {marshalling: true}}, + createValidationPreset({includeValidation: generator.includeValidation}, context) ] }); +} + +function appendOpenAPISerializerFunctions( + result: {models: OutputModel[]; files: GeneratedFile[]}, + headerFunctions: Record +): void { + for (const model of result.models) { + if (!(model.model instanceof ConstrainedObjectModel)) { + continue; + } + const constrainedModel = model.model as ConstrainedObjectModel; + const fns = generateOpenAPIHeaderFunctions(constrainedModel); + const modelFileName = `${model.modelName}.ts`; + const fileIndex = result.files.findIndex((f) => + f.path.endsWith(modelFileName) + ); + if (fileIndex !== -1) { + result.files[fileIndex] = { + ...result.files[fileIndex], + content: `${result.files[fileIndex].content}\n\n${fns}` + }; + } + const modelName = constrainedModel.name; + headerFunctions[modelName] = [`serialize${modelName}Headers`]; + } +} + +function deduplicateFiles(files: GeneratedFile[]): GeneratedFile[] { + const uniqueFiles: GeneratedFile[] = []; + const seenPaths = new Map(); + for (const file of files) { + const existingIndex = seenPaths.get(file.path); + if (existingIndex === undefined) { + seenPaths.set(file.path, uniqueFiles.length); + uniqueFiles.push(file); + } else { + uniqueFiles[existingIndex] = file; + } + } + return uniqueFiles; +} + +// Core generator function that works with processed data +export async function generateTypescriptHeadersCore({ + processedData, + context +}: { + processedData: ProcessedHeadersData; + context: TypescriptHeadersContext; +}): Promise<{ + channelModels: Record; + files: GeneratedFile[]; + headerFunctions: Record; +}> { + const {generator, inputType} = context; + const isOpenAPI = inputType === 'openapi'; + const modelinaGenerator = isOpenAPI + ? createOpenAPIHeadersGenerator() + : createAsyncAPIHeadersGenerator(generator, context); const channelModels: Record = {}; - const files: GeneratedFile[] = []; + const allFiles: GeneratedFile[] = []; + const headerFunctions: Record = {}; for (const [channelId, headerData] of Object.entries( processedData.channelHeaders )) { - if (headerData) { - const result = await generateModels({ - generator: modelinaGenerator, - input: headerData.schema, - outputPath: generator.outputPath - }); - channelModels[channelId] = - result.models.length > 0 ? result.models[0] : undefined; - files.push(...result.files); - } else { + if (!headerData) { channelModels[channelId] = undefined; + continue; } - } - - // Deduplicate files by path - const uniqueFiles: GeneratedFile[] = []; - const seenPaths = new Set(); - for (const file of files) { - if (!seenPaths.has(file.path)) { - seenPaths.add(file.path); - uniqueFiles.push(file); + const result = await generateModels({ + generator: modelinaGenerator, + input: headerData.schema, + outputPath: generator.outputPath + }); + if (isOpenAPI) { + appendOpenAPISerializerFunctions(result, headerFunctions); } + channelModels[channelId] = + result.models.length > 0 ? result.models[0] : undefined; + allFiles.push(...result.files); } - return {channelModels, files: uniqueFiles}; + return {channelModels, files: deduplicateFiles(allFiles), headerFunctions}; } // Main generator function that orchestrates input processing and generation @@ -204,14 +243,16 @@ export async function generateTypescriptHeaders( } // Generate models using processed data - const {channelModels, files} = await generateTypescriptHeadersCore({ - processedData, - context - }); + const {channelModels, files, headerFunctions} = + await generateTypescriptHeadersCore({ + processedData, + context + }); return { channelModels, generator, - files + files, + headerFunctions }; } diff --git a/src/codegen/inputs/openapi/generators/headers.ts b/src/codegen/inputs/openapi/generators/headers.ts index e87e0d84..9534e095 100644 --- a/src/codegen/inputs/openapi/generators/headers.ts +++ b/src/codegen/inputs/openapi/generators/headers.ts @@ -1,8 +1,46 @@ /* eslint-disable security/detect-object-injection */ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {ProcessedHeadersData} from '../../../generators/typescript/headers'; -import {pascalCase} from '../../../generators/typescript/utils'; +import { + defaultCodegenTypescriptModelinaOptions, + pascalCase +} from '../../../generators/typescript/utils'; import {deriveOperationId} from '../utils'; +import { + ConstrainedObjectModel, + TS_DESCRIPTION_PRESET, + TypeScriptFileGenerator +} from '@asyncapi/modelina'; + +export function createOpenAPIHeadersGenerator() { + return new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + enumType: 'union', + useJavascriptReservedKeywords: false, + modelType: 'interface', + presets: [TS_DESCRIPTION_PRESET] + }); +} + +export function generateOpenAPIHeaderFunctions( + model: ConstrainedObjectModel +): string { + const modelName = model.name; + + const headerMappings = Object.values(model.properties) + .map((prop) => { + const wireName = prop.unconstrainedPropertyName; + const tsName = prop.propertyName; + return ` if (headers.${tsName} !== undefined) { result['${wireName}'] = String(headers.${tsName}); }`; + }) + .join('\n'); + + return `export function serialize${modelName}Headers(headers: ${modelName}): Record { + const result: Record = {}; +${headerMappings} + return result; +}`; +} // Helper function to convert OpenAPI header schema to JSON Schema function convertHeaderSchemaToJsonSchema(header: any): any { diff --git a/src/codegen/types.ts b/src/codegen/types.ts index 6bd5f3cb..dcfc1c5f 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -164,6 +164,8 @@ export interface HeadersRenderType { generator: GeneratorType; /** Generated files with path and content */ files: GeneratedFile[]; + /** Map from model name to list of exported standalone function names (OpenAPI only) */ + headerFunctions?: Record; } export interface TypesRenderType { result: string; diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 2b1cd45f..1b0a03d2 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -11,6 +11,7 @@ exports[`channels typescript OpenAPI input should generate HTTP client protocol "import {Pet} from './../../../../../payloads/Pet'; import * as FindPetsByStatusAndCategoryResponseModule from './../../../../../payloads/FindPetsByStatusAndCategoryResponse'; import {FindPetsByStatusAndCategoryParameters} from './../../../../../parameters/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../../../../../headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -27,26 +28,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -61,16 +42,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -165,69 +136,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -285,15 +193,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -303,8 +207,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -386,94 +290,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -616,199 +432,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL @@ -1000,7 +623,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -1009,8 +631,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1020,18 +641,11 @@ async function addPet(context: AddPetContext): Promise> } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - 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; + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1097,17 +711,13 @@ async function addPet(context: AddPetContext): Promise> // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, addPet), }; return result; @@ -1123,7 +733,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -1132,8 +741,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1143,18 +751,11 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - 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; + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1220,17 +821,13 @@ async function updatePet(context: UpdatePetContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, updatePet), }; return result; @@ -1246,7 +843,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -1255,8 +852,7 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1266,18 +862,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeParameterUrl(context.parameters, path)); - 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; + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeParameterUrl(context.parameters, path)); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1343,17 +932,13 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; @@ -1975,26 +1560,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -2009,16 +1574,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -2129,69 +1684,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -2249,15 +1741,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -2267,8 +1755,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -2360,94 +1848,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -2590,199 +1990,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL @@ -2972,9 +2179,7 @@ async function handleTokenRefresh( // Generated HTTP Client Functions // ============================================================================ -export interface GetPingRequestContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface GetPingRequestContext extends HttpClientContext {} /** * HTTP GET request to /ping @@ -2982,8 +2187,7 @@ export interface GetPingRequestContext extends HttpClientContext { async function getPingRequest(context: GetPingRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -2993,18 +2197,11 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - 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; + let url = \`\${config.baseUrl}/ping\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -3070,17 +2267,13 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getPingRequest), }; return result; diff --git a/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap index d78d4721..0c589e7e 100644 --- a/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap @@ -1,184 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`headers typescript should work with OpenAPI 2.0 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; exports[`headers typescript should work with OpenAPI 3.0 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; exports[`headers typescript should work with OpenAPI 3.1 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index 54fe1832..ec497e9d 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -602,6 +602,39 @@ describe('channels', () => { files: [] }; + const xRequestIdProperty = new ConstrainedObjectPropertyModel( + 'xRequestId', + 'X-Request-ID', + false, + new ConstrainedStringModel('string', undefined, {}, 'string') + ); + const acceptLanguageProperty = new ConstrainedObjectPropertyModel( + 'acceptLanguage', + 'Accept-Language', + false, + new ConstrainedStringModel('string', undefined, {}, 'string') + ); + const findPetsByStatusAndCategoryHeadersModel = new OutputModel( + '', + new ConstrainedObjectModel('FindPetsByStatusAndCategoryHeaders', undefined, {}, 'FindPetsByStatusAndCategoryHeaders', { + xRequestId: xRequestIdProperty, + acceptLanguage: acceptLanguageProperty + }), + 'FindPetsByStatusAndCategoryHeaders', + {models: {}, originalInput: undefined}, + [] + ); + const headersDependency: TypeScriptHeadersRenderType = { + channelModels: { + findPetsByStatusAndCategory: findPetsByStatusAndCategoryHeadersModel + }, + generator: {outputPath: './headers'} as any, + files: [], + headerFunctions: { + FindPetsByStatusAndCategoryHeaders: ['serializeFindPetsByStatusAndCategoryHeadersHeaders'] + } + }; + const generatedChannels = await generateTypeScriptChannels({ generator: { ...defaultTypeScriptChannelsGenerator, @@ -615,7 +648,7 @@ describe('channels', () => { dependencyOutputs: { 'parameters-typescript': parametersDependency, 'payloads-typescript': payloadsDependency, - 'headers-typescript': createHeadersDependency() + 'headers-typescript': headersDependency } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts index 43f99206..97a8143c 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts @@ -29,7 +29,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -64,7 +64,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -109,7 +109,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'basic', username: USERNAME, @@ -149,7 +149,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: BEARER_TOKEN @@ -176,7 +176,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'wrong-api-key', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts index 646ed2c3..f42b5391 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'bearer', token: 'test-token-123' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -54,7 +54,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'basic', username: 'user', password: 'pass' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -82,7 +82,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -112,7 +112,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -142,7 +142,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -167,14 +167,14 @@ describe('HTTP Client - Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'secret-key', name: 'api_key', in: 'query' }, - queryParams: { + additionalQueryParams: { filter: 'active' } }); @@ -206,7 +206,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -235,7 +235,7 @@ describe('HTTP Client - Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'my-token' }, additionalHeaders: { 'X-Custom-Header': 'custom-value', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts index 900ab24a..5bb6efc9 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -39,33 +39,6 @@ describe('HTTP Client - Basics', () => { expect(response.rawData).toBeDefined(); }); }); - - it('should include pagination info from response headers', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({ additionalProperties: new Map([['page', 1]]) }); - - router.get('/ping', (req, res) => { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.setHeader('X-Has-More', 'true'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(response.pagination).toBeDefined(); - expect(response.pagination?.totalCount).toBe(100); - expect(response.pagination?.hasMore).toBe(true); - expect(response.pagination?.currentOffset).toBe(0); - expect(response.pagination?.limit).toBe(20); - }); - }); }); describe('query parameters', () => { @@ -86,8 +59,8 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - queryParams: { + baseUrl: `http://localhost:${actualPort}`, + additionalQueryParams: { filter: 'active', sort: 'name' } @@ -109,7 +82,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Unauthorized'); }); }); @@ -123,7 +96,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Forbidden'); }); }); @@ -137,7 +110,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); @@ -151,7 +124,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts index 60d094ae..3864aad5 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts @@ -36,7 +36,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -72,7 +72,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -106,7 +106,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -136,7 +136,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -169,7 +169,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -209,7 +209,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -243,7 +243,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -271,7 +271,7 @@ describe('HTTP Client - Hooks', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks })).rejects.toThrow(/Request to.*failed/); }); @@ -297,7 +297,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -336,7 +336,7 @@ describe('HTTP Client - Hooks', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -381,7 +381,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -426,7 +426,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks, retry }); @@ -436,44 +436,5 @@ describe('HTTP Client - Hooks', () => { expect(requestCount).toBe(2); }); }); - - it('should work with hooks and pagination', async () => { - const { app, router, port } = createTestServer(); - - const hookCalls: { method: string; offset?: string }[] = []; - - router.get('/ping', (req, res) => { - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '60'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const hooks: HttpHooks = { - beforeRequest: (params) => { - const url = new URL(params.url); - hookCalls.push({ - method: params.method, - offset: url.searchParams.get('offset') || undefined - }); - return params; - } - }; - - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - hooks, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - await page1.getNextPage!(); - - expect(hookCalls.length).toBe(2); - expect(hookCalls[0].offset).toBe('0'); - expect(hookCalls[1].offset).toBe('20'); - }); - }); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts index dc5a2f87..9a061a9a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts @@ -31,7 +31,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('GET'); @@ -60,7 +60,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -91,7 +91,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -118,7 +118,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, auth: { type: 'bearer', token: 'put-token' } }); @@ -143,9 +143,9 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, - queryParams: { version: '2' } + additionalQueryParams: { version: '2' } }); expect(receivedQuery.version).toBe('2'); @@ -169,7 +169,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('DELETE'); @@ -193,7 +193,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'delete-token' } }); @@ -221,7 +221,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -231,33 +231,6 @@ describe('HTTP Client - HTTP Methods', () => { expect(response.data).toBeDefined(); }); }); - - it('should support PATCH with pagination', async () => { - const { app, router, port } = createTestServer(); - - const requestMessage = new Ping({}); - const replyMessage = new Pong({}); - let receivedOffset: string | undefined; - - router.patch('/ping', (req, res) => { - receivedOffset = req.query.offset as string; - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '50'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, - payload: requestMessage, - pagination: { type: 'offset', offset: 10, limit: 5 } - }); - - expect(receivedOffset).toBe('10'); - expect(response.pagination).toBeDefined(); - }); - }); }); describe('HEAD method', () => { @@ -278,7 +251,7 @@ describe('HTTP Client - HTTP Methods', () => { // This test verifies the method is sent correctly even if parsing fails try { await headPingHeadRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); } catch (error) { // Expected - HEAD responses have no body to parse @@ -304,7 +277,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { try { await headPingHeadRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'head-key' } }); } catch (error) { @@ -334,7 +307,7 @@ describe('HTTP Client - HTTP Methods', () => { // OPTIONS requests typically return no body try { await optionsPingOptionsRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); } catch (error) { // Expected - OPTIONS responses typically have no body @@ -357,7 +330,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Not Found'); }); @@ -372,7 +345,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(deletePingDeleteRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); @@ -388,7 +361,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Forbidden'); }); @@ -416,7 +389,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [503] } }); @@ -445,7 +418,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [502] } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts index 16b448e4..dfcae0b1 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts @@ -50,7 +50,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -76,7 +76,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Client Credentials flow requires tokenUrl'); }); @@ -120,7 +120,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -147,7 +147,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Password flow requires username'); }); @@ -194,7 +194,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -262,7 +262,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -324,7 +324,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -383,7 +383,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -438,7 +438,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry })).rejects.toThrow(); @@ -494,7 +494,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -538,7 +538,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -571,7 +571,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -607,7 +607,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -654,7 +654,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts index feae72fb..e71dddbf 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts @@ -75,7 +75,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -116,7 +116,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -139,7 +139,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { // Using as any to bypass TypeScript's type checking for this test await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + baseUrl: `http://localhost:${port}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -216,7 +216,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts index 4a836c8f..287cbe16 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts @@ -63,7 +63,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // (implicit, authorization_code via PKCE, etc.) const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -126,7 +126,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // Use pre-obtained token with refresh capability const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, @@ -164,7 +164,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // Simplest OAuth2 usage - just pass the access token const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -194,7 +194,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // OAuth2 config without access token and no server-side flow await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2' // No accessToken, no flow - should make request without auth header diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts index 623f1672..dc86581d 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts @@ -75,7 +75,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -121,7 +121,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -148,7 +148,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { // Using as any to bypass TypeScript's type checking for this test await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + baseUrl: `http://localhost:${port}`, auth: { type: 'oauth2', flow: 'password', @@ -214,7 +214,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts index bcd2a6c8..ee2b9183 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts @@ -90,7 +90,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, @@ -142,7 +142,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, @@ -176,7 +176,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, @@ -242,7 +242,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts index 3cfe72a5..bbec06ec 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts @@ -29,7 +29,7 @@ describe('HTTP Client - OpenAPI Generated', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -58,7 +58,7 @@ describe('HTTP Client - OpenAPI Generated', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await updatePet({ payload: requestPet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('PUT'); @@ -90,7 +90,7 @@ describe('HTTP Client - OpenAPI Generated', () => { const response = await findPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedPath).toContain('available'); @@ -112,7 +112,7 @@ describe('HTTP Client - OpenAPI Generated', () => { const pet = new APet({ name: 'Test', photoUrls: [] }); await expect(addPet({ payload: pet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow(); }); }); @@ -132,7 +132,7 @@ describe('HTTP Client - OpenAPI Generated', () => { await expect(findPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts deleted file mode 100644 index ee74bc9f..00000000 --- a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -/* eslint-disable no-console */ -import { Pong } from "../../../../src/request-reply/payloads/Pong"; -import { createTestServer, runWithServer } from './test-utils'; -import { - getPingGetRequest, - PaginationConfig, -} from '../../../../src/request-reply/channels/http_client'; - -jest.setTimeout(15000); - -describe('HTTP Client - Pagination', () => { - describe('offset pagination', () => { - it('should add offset pagination params to query string', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedOffset: string | undefined; - let receivedLimit: string | undefined; - - router.get('/ping', (req, res) => { - receivedOffset = req.query.offset as string; - receivedLimit = req.query.limit as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - offset: 20, - limit: 10 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedOffset).toBe('20'); - expect(receivedLimit).toBe('10'); - }); - }); - - it('should add pagination params to headers when in: header', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedOffsetHeader: string | undefined; - let receivedLimitHeader: string | undefined; - - router.get('/ping', (req, res) => { - receivedOffsetHeader = req.headers['x-offset'] as string; - receivedLimitHeader = req.headers['x-limit'] as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - in: 'header', - offset: 50, - limit: 25 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedOffsetHeader).toBe('50'); - expect(receivedLimitHeader).toBe('25'); - }); - }); - - it('should use custom pagination param names', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedSkip: string | undefined; - let receivedTake: string | undefined; - - router.get('/ping', (req, res) => { - receivedSkip = req.query.skip as string; - receivedTake = req.query.take as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - offset: 100, - limit: 50, - offsetParam: 'skip', - limitParam: 'take' - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedSkip).toBe('100'); - expect(receivedTake).toBe('50'); - }); - }); - }); - - describe('cursor pagination', () => { - it('should add cursor pagination params', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedCursor: string | undefined; - let receivedLimit: string | undefined; - - router.get('/ping', (req, res) => { - receivedCursor = req.query.cursor as string; - receivedLimit = req.query.limit as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'cursor', - cursor: 'abc123xyz', - limit: 15 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedCursor).toBe('abc123xyz'); - expect(receivedLimit).toBe('15'); - }); - }); - - it('should handle cursor-based pagination with next cursor from headers', async () => { - const { app, router, port } = createTestServer(); - - const cursors: (string | undefined)[] = []; - - router.get('/ping', (req, res) => { - const cursor = req.query.cursor as string | undefined; - cursors.push(cursor); - - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - - if (!cursor) { - res.setHeader('X-Next-Cursor', 'cursor-page-2'); - } else if (cursor === 'cursor-page-2') { - res.setHeader('X-Next-Cursor', 'cursor-page-3'); - } - - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'cursor', limit: 10 } - }); - - expect(page1.pagination?.nextCursor).toBe('cursor-page-2'); - expect(page1.hasNextPage?.()).toBe(true); - - const page2 = await page1.getNextPage!(); - expect(page2.pagination?.nextCursor).toBe('cursor-page-3'); - - const page3 = await page2.getNextPage!(); - expect(page3.pagination?.nextCursor).toBeUndefined(); - expect(page3.hasNextPage?.()).toBe(false); - - expect(cursors).toEqual([undefined, 'cursor-page-2', 'cursor-page-3']); - }); - }); - }); - - describe('page pagination', () => { - it('should add page-based pagination params', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedPage: string | undefined; - let receivedPageSize: string | undefined; - - router.get('/ping', (req, res) => { - receivedPage = req.query.page as string; - receivedPageSize = req.query.pageSize as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'page', - page: 3, - pageSize: 25 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedPage).toBe('3'); - expect(receivedPageSize).toBe('25'); - }); - }); - }); - - describe('range pagination', () => { - it('should add Range header for range pagination', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedRangeHeader: string | undefined; - - router.get('/ping', (req, res) => { - receivedRangeHeader = req.headers['range'] as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'range', - start: 0, - end: 24, - unit: 'items' - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedRangeHeader).toBe('items=0-24'); - }); - }); - - it('should handle range pagination for page navigation', async () => { - const { app, router, port } = createTestServer(); - - const ranges: string[] = []; - - router.get('/ping', (req, res) => { - ranges.push(req.headers['range'] as string); - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'range', start: 0, end: 24, unit: 'items' } - }); - - expect(page1.hasNextPage?.()).toBe(true); - - await page1.getNextPage!(); - - expect(ranges).toEqual(['items=0-24', 'items=25-49']); - }); - }); - }); - - describe('pagination helpers', () => { - it('should provide pagination helpers when pagination is configured', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - - router.get('/ping', (req, res) => { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(response.hasNextPage).toBeDefined(); - expect(response.hasPrevPage).toBeDefined(); - expect(response.getNextPage).toBeDefined(); - expect(response.getPrevPage).toBeDefined(); - expect(response.hasNextPage?.()).toBe(true); - expect(response.hasPrevPage?.()).toBe(false); - }); - }); - - it('should navigate through pages with getNextPage', async () => { - const { app, router, port } = createTestServer(); - - let requestCount = 0; - const offsets: string[] = []; - - router.get('/ping', (req, res) => { - requestCount++; - offsets.push(req.query.offset as string); - const replyMessage = new Pong({ additionalProperties: new Map([['page', requestCount]]) }); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(page1.hasNextPage?.()).toBe(true); - - const page2 = await page1.getNextPage!(); - expect(page2.pagination?.currentOffset).toBe(20); - - const page3 = await page2.getNextPage!(); - expect(page3.pagination?.currentOffset).toBe(40); - - expect(requestCount).toBe(3); - expect(offsets).toEqual(['0', '20', '40']); - }); - }); - - it('should navigate backwards with getPrevPage', async () => { - const { app, router, port } = createTestServer(); - - const offsets: string[] = []; - - router.get('/ping', (req, res) => { - offsets.push(req.query.offset as string); - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 60, limit: 20 } - }); - - expect(page.hasPrevPage?.()).toBe(true); - - const prevPage = await page.getPrevPage!(); - expect(prevPage.pagination?.currentOffset).toBe(40); - - expect(offsets).toEqual(['60', '40']); - }); - }); - - it('should parse Link header for pagination info', async () => { - const { app, router, port } = createTestServer(); - - router.get('/ping', (req, res) => { - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Link', '; rel="next", ; rel="last"'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'page', page: 1, pageSize: 20 } - }); - - expect(response.pagination?.hasMore).toBe(true); - }); - }); - }); -}); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts index 57ccf4a7..2f0091b3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts @@ -34,7 +34,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters }); @@ -63,12 +63,12 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'alice', itemId: '100' }) }); await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'bob', itemId: '200' }) }); @@ -98,7 +98,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'secure-user', itemId: '999' }), auth: { type: 'bearer', token: 'secret-token' } }); @@ -130,9 +130,9 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user1', itemId: '42' }), - queryParams: { + additionalQueryParams: { include: 'metadata', fields: 'name,quantity' } @@ -177,7 +177,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user-1', itemId: '100' }), payload, requestHeaders: headers @@ -216,7 +216,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u1', itemId: '1' }), payload, requestHeaders: headers @@ -247,7 +247,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'corr-id' }), @@ -281,7 +281,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Secure Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'secure-corr' }), @@ -328,7 +328,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'full-user', itemId: '999' }), payload: new ItemRequest({ name: 'Complete Item', @@ -340,7 +340,7 @@ describe('HTTP Client - Parameters and Headers', () => { xRequestId: 'full-req-id' }), auth: { type: 'basic', username: 'admin', password: 'secret' }, - queryParams: { verbose: 'true' } + additionalQueryParams: { verbose: 'true' } }); expect(receivedData).toBeDefined(); @@ -372,7 +372,7 @@ describe('HTTP Client - Parameters and Headers', () => { try { const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters }); expect(response.status).toBe(404); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts index 9ad51ebc..18145c4a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts @@ -23,7 +23,7 @@ describe('HTTP Client - primitive response payloads', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const response = await getEcho({ server: `http://localhost:${actualPort}` }); + const response = await getEcho({ baseUrl: `http://localhost:${actualPort}` }); expect(response.status).toBe(200); expect(response.data).toBe('hello world'); @@ -39,7 +39,7 @@ describe('HTTP Client - primitive response payloads', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const response = await getCount({ server: `http://localhost:${actualPort}` }); + const response = await getCount({ baseUrl: `http://localhost:${actualPort}` }); expect(response.status).toBe(200); expect(response.data).toBe(42); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts index 586a07c3..083d0c8b 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts @@ -35,7 +35,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -73,7 +73,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -114,7 +114,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -143,7 +143,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -183,7 +183,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -225,7 +225,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -254,7 +254,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow(); @@ -286,7 +286,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -313,7 +313,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -345,7 +345,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -378,7 +378,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -394,7 +394,7 @@ describe('HTTP Client - Retry Logic', () => { const unusedPort = 59999; await expect(getPingGetRequest({ - server: `http://localhost:${unusedPort}` + baseUrl: `http://localhost:${unusedPort}` })).rejects.toThrow(); }); @@ -411,7 +411,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${unusedPort}`, + baseUrl: `http://localhost:${unusedPort}`, retry })).rejects.toThrow(); @@ -440,7 +440,7 @@ describe('HTTP Client - Retry Logic', () => { // Request should succeed on first try const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts index 65993115..b6a90bd7 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts @@ -84,7 +84,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // from the spec are documented in the generated interface await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key' @@ -123,7 +123,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // Use the header name from the spec await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key', @@ -162,7 +162,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // With a pre-obtained token, oauth2 works const response = await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'pre-obtained-token' @@ -198,7 +198,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // The generated interface documents these in the JSDoc await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'token', @@ -219,13 +219,13 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // // await addPet({ // payload: requestPet, - // server: `http://localhost:${actualPort}`, + // baseUrl: `http://localhost:${actualPort}`, // auth: { type: 'basic', username: 'user', password: 'pass' } // TypeScript Error! // }); // // await addPet({ // payload: requestPet, - // server: `http://localhost:${actualPort}`, + // baseUrl: `http://localhost:${actualPort}`, // auth: { type: 'bearer', token: 'token' } // TypeScript Error! // });