From cd0f25fb56f02556c55d44721e61eb41d9697f9d Mon Sep 17 00:00:00 2001 From: Lagoni <68890168+ALagoni97@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:26:42 +0200 Subject: [PATCH 1/5] Phase 1: remove pagination from HTTP client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletes PaginationInfo, PaginationConfig and all pagination subtypes, applyPagination, extractPaginationInfo, parseLinkHeader, and createPaginationHelpers from the runtime template. Removes pagination fields from HttpClientResponse and HttpClientContext. Deletes the pagination runtime spec and strips pagination sections from docs. The current pagination was client-side invention (offset/limit/cursor params not derived from the spec) — a future spec-derived implementation is deferred; see thoughts/2026-07-14-http-client-context-cleanup/plan.md. --- docs/generators/client.md | 4 +- docs/protocols/http_client.md | 81 +- .../channels/protocols/http/client.ts | 9 - .../channels/protocols/http/common-types.ts | 377 --------- .../__snapshots__/channels.spec.ts.snap | 790 ------------------ .../http_client/pagination.spec.ts | 396 --------- 6 files changed, 3 insertions(+), 1654 deletions(-) delete mode 100644 test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts diff --git a/docs/generators/client.md b/docs/generators/client.md index 2401cc57..5f829c09 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 (`server`, `auth`, `hooks`, retry, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. ```js export default { @@ -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..78276793 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) | @@ -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: diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 98d43c74..3c38732e 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -266,11 +266,6 @@ ${oauth2ValidateBlock} // Build headers ${urlBuildCode} url = applyQueryParams(config.queryParams, url); - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; - // Apply authentication const authResult = applyAuth(config.auth, headers, url); headers = authResult.headers; @@ -315,17 +310,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..2332ee1c 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 // ============================================================================ @@ -264,9 +171,6 @@ export interface HttpClientContext { // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -337,94 +241,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 +383,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/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 2b1cd45f..791e425d 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -27,26 +27,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 +41,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 +135,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 // ============================================================================ @@ -291,9 +198,6 @@ export interface HttpClientContext { // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -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 @@ -1028,11 +651,6 @@ async function addPet(context: AddPetContext): Promise> 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; - // Apply authentication const authResult = applyAuth(config.auth, headers, url); headers = authResult.headers; @@ -1097,17 +715,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; @@ -1151,11 +765,6 @@ 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; @@ -1274,11 +879,6 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC 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; - // Apply authentication const authResult = applyAuth(config.auth, headers, url); headers = authResult.headers; @@ -1343,17 +943,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 +1571,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 +1585,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 +1695,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 // ============================================================================ @@ -2255,9 +1758,6 @@ export interface HttpClientContext { // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -2360,94 +1860,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 +2002,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 @@ -3001,11 +2220,6 @@ 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/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); - }); - }); - }); -}); From 2d65ebd9055d531104b6bb8b083b3e172ce193bf Mon Sep 17 00:00:00 2001 From: Lagoni <68890168+ALagoni97@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:28:52 +0200 Subject: [PATCH 2/5] Phase 2: remove path from HttpClientContext path was half-broken: parameterized operations already baked the path literal into buildUrlWithParameters and silently ignored config.path; only parameterless operations respected it. Now parameterless ops also bake the literal path at generation time (e.g. `${config.server}/ping`). The escape hatch for gateway rewrites is hooks.beforeRequest mutating params.url, as documented. --- .../typescript/channels/protocols/http/client.ts | 3 +-- .../channels/protocols/http/common-types.ts | 1 - .../typescript/__snapshots__/channels.spec.ts.snap | 12 +++--------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 3c38732e..639eb5c1 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -133,7 +133,7 @@ function buildUrlCode( hasSerializeUrl: boolean ): string { if (!hasParameters || !parametersType) { - return 'let url = `${config.server}${config.path}`;'; + return `let url = \`\${config.server}${requestTopic}\`;`; } const serializeFn = hasSerializeUrl ? `serialize${parametersType}Url(context.parameters, path)` @@ -254,7 +254,6 @@ function generateFunctionImplementation(params: { async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { // Apply defaults const config = { - path: '${requestTopic}', server: ${defaultServer}, ...context, }; 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 2332ee1c..ff5636cb 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -166,7 +166,6 @@ export interface HttpHooks { */ export interface HttpClientContext { server?: string; - path?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 791e425d..1aa3ad6a 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -193,7 +193,6 @@ export interface HttpHooks { */ export interface HttpClientContext { server?: string; - path?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; @@ -632,7 +631,6 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', server: 'localhost:3000', ...context, }; @@ -648,7 +646,7 @@ async function addPet(context: AddPetContext): Promise> : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; + let url = \`\${config.server}/pet\`; url = applyQueryParams(config.queryParams, url); // Apply authentication @@ -746,7 +744,6 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', server: 'localhost:3000', ...context, }; @@ -762,7 +759,7 @@ async function updatePet(context: UpdatePetContext): Promise; // Build URL - let url = \`\${config.server}\${config.path}\`; + let url = \`\${config.server}/pet\`; url = applyQueryParams(config.queryParams, url); // Apply authentication @@ -860,7 +857,6 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', server: 'localhost:3000', ...context, }; @@ -1753,7 +1749,6 @@ export interface HttpHooks { */ export interface HttpClientContext { server?: string; - path?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; @@ -2201,7 +2196,6 @@ export interface GetPingRequestContext extends HttpClientContext { async function getPingRequest(context: GetPingRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', server: 'localhost:3000', ...context, }; @@ -2217,7 +2211,7 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise; // Build URL - let url = \`\${config.server}\${config.path}\`; + let url = \`\${config.server}/ping\`; url = applyQueryParams(config.queryParams, url); // Apply authentication From bd146ec6305312bb453676cfdb21bf1c353d97fa Mon Sep 17 00:00:00 2001 From: Lagoni <68890168+ALagoni97@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:36:06 +0200 Subject: [PATCH 3/5] =?UTF-8?q?Phase=204:=20rename=20server=E2=86=92baseUr?= =?UTF-8?q?l,=20queryParams=E2=86=92additionalQueryParams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renames mirror the additionalHeaders convention: additionalQueryParams makes clear it is an escape hatch for params not in the spec, not a replacement for typed parameters. baseUrl is unambiguous vs the old server field. Also fixes the scheme-less default 'localhost:3000' → 'http://localhost:3000' so generated code works out of the box without a CORS or fetch failure. --- docs/generators/client.md | 4 +-- docs/protocols/http_client.md | 34 +++++++++--------- .../channels/protocols/http/client.ts | 10 +++--- .../channels/protocols/http/common-types.ts | 6 ++-- .../typescript/client/protocols/http.ts | 2 +- .../__snapshots__/channels.spec.ts.snap | 36 +++++++++---------- .../http_client/api_auth.spec.ts | 10 +++--- .../http_client/authentication.spec.ts | 18 +++++----- .../request_reply/http_client/basics.spec.ts | 16 ++++----- .../request_reply/http_client/hooks.spec.ts | 26 +++++++------- .../request_reply/http_client/methods.spec.ts | 36 +++++++++---------- .../request_reply/http_client/oauth2.spec.ts | 28 +++++++-------- .../oauth2_client_credentials.spec.ts | 8 ++--- .../http_client/oauth2_implicit_flow.spec.ts | 8 ++--- .../http_client/oauth2_password_flow.spec.ts | 8 ++--- .../http_client/oauth2_refresh_token.spec.ts | 8 ++--- .../request_reply/http_client/openapi.spec.ts | 10 +++--- .../http_client/parameters-headers.spec.ts | 26 +++++++------- .../http_client/primitive-response.spec.ts | 4 +-- .../request_reply/http_client/retry.spec.ts | 28 +++++++-------- .../http_client/security_schemes.spec.ts | 12 +++---- 21 files changed, 169 insertions(+), 169 deletions(-) diff --git a/docs/generators/client.md b/docs/generators/client.md index 5f829c09..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, ...) 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 ?? ''} }); diff --git a/docs/protocols/http_client.md b/docs/protocols/http_client.md index 78276793..236c67e3 100644 --- a/docs/protocols/http_client.md +++ b/docs/protocols/http_client.md @@ -114,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 @@ -143,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); @@ -167,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' @@ -180,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', @@ -195,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', @@ -207,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', @@ -224,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', @@ -247,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', @@ -273,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, @@ -295,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) @@ -317,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) => { @@ -380,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 }); ``` @@ -398,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 @@ -412,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' } @@ -440,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/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 639eb5c1..5e9f20d8 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -133,12 +133,12 @@ function buildUrlCode( hasSerializeUrl: boolean ): string { if (!hasParameters || !parametersType) { - return `let url = \`\${config.server}${requestTopic}\`;`; + 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});`; } /** @@ -179,7 +179,7 @@ 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()); @@ -254,7 +254,7 @@ function generateFunctionImplementation(params: { async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { // Apply defaults const config = { - server: ${defaultServer}, + baseUrl: ${defaultServer}, ...context, }; @@ -263,7 +263,7 @@ ${oauth2ValidateBlock} // Build headers // Build URL ${urlBuildCode} - url = applyQueryParams(config.queryParams, url); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); 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 ff5636cb..2fcb6371 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -165,7 +165,7 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; @@ -179,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; } // ============================================================================ 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/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 1aa3ad6a..2837e82c 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -192,7 +192,7 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; @@ -206,8 +206,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -631,7 +631,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -646,8 +646,8 @@ async function addPet(context: AddPetContext): Promise> : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}/pet\`; - url = applyQueryParams(config.queryParams, url); + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -744,7 +744,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -759,8 +759,8 @@ async function updatePet(context: UpdatePetContext): Promise; // Build URL - let url = \`\${config.server}/pet\`; - url = applyQueryParams(config.queryParams, url); + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -857,7 +857,7 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -872,8 +872,8 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeParameterUrl(context.parameters, path)); - url = applyQueryParams(config.queryParams, url); + 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); @@ -1748,7 +1748,7 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; @@ -1762,8 +1762,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -2196,7 +2196,7 @@ export interface GetPingRequestContext extends HttpClientContext { async function getPingRequest(context: GetPingRequestContext = {}): Promise> { // Apply defaults const config = { - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -2211,8 +2211,8 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise; // Build URL - let url = \`\${config.server}/ping\`; - url = applyQueryParams(config.queryParams, url); + let url = \`\${config.baseUrl}/ping\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); 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..9eaccd8c 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(); @@ -55,7 +55,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, pagination: { type: 'offset', offset: 0, limit: 20 } }); @@ -86,8 +86,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 +109,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 +123,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 +137,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 +151,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..a716bf59 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 }); @@ -463,7 +463,7 @@ describe('HTTP Client - Hooks', () => { }; const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks, pagination: { type: 'offset', offset: 0, limit: 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..c8445c76 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 }); @@ -249,7 +249,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, pagination: { type: 'offset', offset: 10, limit: 5 } }); @@ -278,7 +278,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 +304,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 +334,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 +357,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 +372,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 +388,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 +416,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 +445,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/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! // }); From d9b86d7043ba1284f749adc191da19e875361357 Mon Sep 17 00:00:00 2001 From: Lagoni <68890168+ALagoni97@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:08:57 +0200 Subject: [PATCH 4/5] Phase 3: conditional requestHeaders + plain-object OpenAPI header models 3a: Emit requestHeaders in context interface only when the spec defines operation-level headers; operations with no spec headers get a plain headersInit instead of the applyTypedHeaders branch. 3b: OpenAPI header models are now generated as plain interfaces (no marshal/unmarshal/validate) with a standalone serializeXxxHeaders() function. The channels generator imports and calls that function when hasSerializeHeaders is true. --- .../generators/typescript/channels/openapi.ts | 12 +- .../channels/protocols/http/client.ts | 35 +++- .../generators/typescript/channels/types.ts | 6 + .../generators/typescript/channels/utils.ts | 13 +- src/codegen/generators/typescript/headers.ts | 113 +++++++---- .../inputs/openapi/generators/headers.ts | 40 +++- src/codegen/types.ts | 2 + .../__snapshots__/channels.spec.ts.snap | 25 +-- .../__snapshots__/headers.spec.ts.snap | 177 +----------------- .../generators/typescript/channels.spec.ts | 35 +++- 10 files changed, 221 insertions(+), 237 deletions(-) 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 5e9f20d8..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` : ''; @@ -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, @@ -186,9 +196,16 @@ function generateFunctionImplementation(params: { 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 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/headers.ts b/src/codegen/generators/typescript/headers.ts index fd0d1cd0..026086fc 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, @@ -109,37 +114,42 @@ export async function generateTypescriptHeadersCore({ }): Promise<{ channelModels: Record; files: GeneratedFile[]; + headerFunctions: Record; }> { - const {generator} = context; - - const modelinaGenerator = new TypeScriptFileGenerator({ - ...defaultCodegenTypescriptModelinaOptions, - constraints: { - propertyKey: typeScriptDefaultPropertyKeyConstraints({ - NO_SPECIAL_CHAR: (value) => value.replace(/[^a-zA-Z0-9]/g, '_') - }) - }, - enumType: 'union', - useJavascriptReservedKeywords: false, - presets: [ - TS_DESCRIPTION_PRESET, - { - preset: TS_COMMON_PRESET, - options: { - marshalling: true - } - }, - createValidationPreset( - { - includeValidation: generator.includeValidation + const {generator, inputType} = context; + const isOpenAPI = inputType === 'openapi'; + + const modelinaGenerator = isOpenAPI + ? createOpenAPIHeadersGenerator() + : new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + constraints: { + propertyKey: typeScriptDefaultPropertyKeyConstraints({ + NO_SPECIAL_CHAR: (value) => value.replace(/[^a-zA-Z0-9]/g, '_') + }) }, - context - ) - ] - }); + enumType: 'union', + useJavascriptReservedKeywords: false, + presets: [ + TS_DESCRIPTION_PRESET, + { + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } + }, + createValidationPreset( + { + includeValidation: generator.includeValidation + }, + context + ) + ] + }); const channelModels: Record = {}; const files: GeneratedFile[] = []; + const headerFunctions: Record = {}; for (const [channelId, headerData] of Object.entries( processedData.channelHeaders @@ -150,6 +160,30 @@ export async function generateTypescriptHeadersCore({ input: headerData.schema, outputPath: generator.outputPath }); + + if (isOpenAPI) { + for (const model of result.models) { + if (model.model instanceof ConstrainedObjectModel) { + 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`]; + } + } + } + channelModels[channelId] = result.models.length > 0 ? result.models[0] : undefined; files.push(...result.files); @@ -158,17 +192,22 @@ export async function generateTypescriptHeadersCore({ } } - // Deduplicate files by path + // Deduplicate files by path, keeping the last version (which has functions appended) const uniqueFiles: GeneratedFile[] = []; - const seenPaths = new Set(); + const seenPaths = new Map(); for (const file of files) { if (!seenPaths.has(file.path)) { - seenPaths.add(file.path); + seenPaths.set(file.path, uniqueFiles.length); uniqueFiles.push(file); + } else { + const existingIndex = seenPaths.get(file.path); + if (existingIndex !== undefined) { + uniqueFiles[existingIndex] = file; + } } } - return {channelModels, files: uniqueFiles}; + return {channelModels, files: uniqueFiles, 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 2837e82c..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 @@ -622,7 +623,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -641,9 +641,7 @@ 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.baseUrl}/pet\`; @@ -735,7 +733,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -754,9 +751,7 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL let url = \`\${config.baseUrl}/pet\`; @@ -848,7 +843,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -867,9 +862,7 @@ 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.baseUrl, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeParameterUrl(context.parameters, path)); @@ -2186,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 @@ -2206,9 +2197,7 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL let url = \`\${config.baseUrl}/ping\`; 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 } }); From 772686fedd784105bf6580e12b91f4e9f4758821 Mon Sep 17 00:00:00 2001 From: Lagoni <68890168+ALagoni97@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:54:23 +0200 Subject: [PATCH 5/5] fix: remove leftover pagination tests and fix lint complexity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove three pagination test cases missed from methods.spec.ts, hooks.spec.ts, and basics.spec.ts when pagination.spec.ts was deleted. Refactor generateTypescriptHeadersCore into three extracted helpers (createAsyncAPIHeadersGenerator, appendOpenAPISerializerFunctions, deduplicateFiles) to bring cognitive complexity within the lint limit. Also fix pre-existing TypescryptHeadersContext typo → TypescriptHeadersContext. --- src/codegen/generators/typescript/headers.ts | 152 +++++++++--------- .../request_reply/http_client/basics.spec.ts | 27 ---- .../request_reply/http_client/hooks.spec.ts | 39 ----- .../request_reply/http_client/methods.spec.ts | 27 ---- 4 files changed, 76 insertions(+), 169 deletions(-) diff --git a/src/codegen/generators/typescript/headers.ts b/src/codegen/generators/typescript/headers.ts index 026086fc..4b234216 100644 --- a/src/codegen/generators/typescript/headers.ts +++ b/src/codegen/generators/typescript/headers.ts @@ -104,6 +104,67 @@ export interface ProcessedHeadersData { >; } +function createAsyncAPIHeadersGenerator( + generator: TypescriptHeadersGeneratorInternal, + context: TypescriptHeadersContext +): TypeScriptFileGenerator { + return new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + constraints: { + propertyKey: typeScriptDefaultPropertyKeyConstraints({ + NO_SPECIAL_CHAR: (value) => value.replace(/[^a-zA-Z0-9]/g, '_') + }) + }, + enumType: 'union', + useJavascriptReservedKeywords: false, + presets: [ + TS_DESCRIPTION_PRESET, + {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, @@ -118,96 +179,35 @@ export async function generateTypescriptHeadersCore({ }> { const {generator, inputType} = context; const isOpenAPI = inputType === 'openapi'; - const modelinaGenerator = isOpenAPI ? createOpenAPIHeadersGenerator() - : new TypeScriptFileGenerator({ - ...defaultCodegenTypescriptModelinaOptions, - constraints: { - propertyKey: typeScriptDefaultPropertyKeyConstraints({ - NO_SPECIAL_CHAR: (value) => value.replace(/[^a-zA-Z0-9]/g, '_') - }) - }, - enumType: 'union', - useJavascriptReservedKeywords: false, - presets: [ - TS_DESCRIPTION_PRESET, - { - preset: TS_COMMON_PRESET, - options: { - marshalling: true - } - }, - createValidationPreset( - { - includeValidation: generator.includeValidation - }, - context - ) - ] - }); + : 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 - }); - - if (isOpenAPI) { - for (const model of result.models) { - if (model.model instanceof ConstrainedObjectModel) { - 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`]; - } - } - } - - 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, keeping the last version (which has functions appended) - const uniqueFiles: GeneratedFile[] = []; - const seenPaths = new Map(); - for (const file of files) { - if (!seenPaths.has(file.path)) { - seenPaths.set(file.path, uniqueFiles.length); - uniqueFiles.push(file); - } else { - const existingIndex = seenPaths.get(file.path); - if (existingIndex !== undefined) { - uniqueFiles[existingIndex] = 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, headerFunctions}; + return {channelModels, files: deduplicateFiles(allFiles), headerFunctions}; } // Main generator function that orchestrates input processing and generation 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 9eaccd8c..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 @@ -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({ - baseUrl: `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', () => { 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 a716bf59..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 @@ -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({ - baseUrl: `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 c8445c76..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 @@ -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({ - baseUrl: `http://localhost:${actualPort}`, - payload: requestMessage, - pagination: { type: 'offset', offset: 10, limit: 5 } - }); - - expect(receivedOffset).toBe('10'); - expect(response.pagination).toBeDefined(); - }); - }); }); describe('HEAD method', () => {