diff --git a/src/apis/V1Api.ts b/src/apis/V1Api.ts index 1c5c58e..e649b1d 100644 --- a/src/apis/V1Api.ts +++ b/src/apis/V1Api.ts @@ -1472,12 +1472,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - JournalistSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return JournalistSchema.parse(raw); } - /** * Search and filter all news articles available via the Perigon API. The result includes a list of individual articles that were matched to your specific criteria. * Articles @@ -1509,12 +1506,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - QuerySearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return QuerySearchResultSchema.parse(raw); } - /** * Browse or search for companies Perigon tracks using name, domain, ticker symbol, industry, and more. Supports Boolean search logic and filtering by metadata such as country, exchange, employee count, and IPO date. * Companies @@ -1546,12 +1540,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - CompanySearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return CompanySearchResultSchema.parse(raw); } - /** * Search journalists using broad search attributes. Our database contains over 230,000 journalists from around the world and is refreshed frequently. * Journalists @@ -1583,12 +1574,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - JournalistSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return JournalistSearchResultSchema.parse(raw); } - /** * Search and retrieve additional information on known persons that exist within Perigon\'s entity database and as referenced in any article response object. Our database contains over 650,000 people from around the world and is refreshed frequently. People data is derived from Wikidata and includes a wikidataId field that can be used to lookup even more information on Wikidata\'s website. * People @@ -1620,12 +1608,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - PeopleSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return PeopleSearchResultSchema.parse(raw); } - /** * Search and filter the 142,000+ media sources available via the Perigon API. The result includes a list of individual media sources that were matched to your specific criteria. * Sources @@ -1657,12 +1642,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - SourceSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return SourceSearchResultSchema.parse(raw); } - /** * Search and filter all news stories available via the Perigon API. Each story aggregates key information across related articles, including AI-generated names, summaries, and key points. * Stories @@ -1694,12 +1676,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - StorySearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return StorySearchResultSchema.parse(raw); } - /** * Produce a single, concise summary over the full corpus of articles matching your filters, using your prompt to guide which insights to highlight. * Search Summarizer @@ -1733,12 +1712,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - SummarySearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return SummarySearchResultSchema.parse(raw); } - /** * Search through all available Topics that exist within the Perigon Database. * Topics @@ -1770,12 +1746,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - TopicSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return TopicSearchResultSchema.parse(raw); } - /** * Search and filter all Wikipedia pages available via the Perigon API. The result includes a list of individual pages that were matched to your specific criteria. * Wikipedia @@ -1807,12 +1780,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - WikipediaSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return WikipediaSearchResultSchema.parse(raw); } - /** * Perform a natural language search over news articles from the past 6 months using semantic relevance. The result includes a list of articles most closely matched to your query intent. * Vector @@ -1845,12 +1815,9 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - ArticlesVectorSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return ArticlesVectorSearchResultSchema.parse(raw); } - /** * Perform a natural language search over Wikipedia pages using semantic relevance. The result includes a list of page sections most closely matched to your query intent. * Vector @@ -1883,9 +1850,7 @@ export class V1Api extends runtime.BaseAPI { initOverrides, ); - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => - WikipediaVectorSearchResultSchema.parse(jsonValue), - ); - return await apiResponse.value(); + const raw = await response.json(); + return WikipediaVectorSearchResultSchema.parse(raw); } } diff --git a/src/models/index.ts b/src/models/index.ts index 2441bb4..e467c7a 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -563,11 +563,19 @@ export const ArticleSearchParamsSchema = z.object({ /** * 'pubDateFrom' filter, will search articles published after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Date time in ISO format, ie. 2024-01-01T00:00:00 - Default: Only articles with a pubDate within the last 30 days of the request */ - pubDateFrom: z.string().optional().nullable(), + pubDateFrom: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), /** * 'pubDateFrom' filter, will search articles published before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Date time in ISO format, ie. 2024-01-01T00:00:00 */ - pubDateTo: z.string().optional().nullable(), + pubDateTo: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), /** * Whether to return reprints in the response or not. Reprints are usually wired articles from sources like AP or Reuters that are reprinted in multiple sources at the same time. By default, this parameter is 'true'. */ @@ -1187,8 +1195,16 @@ export type TopicLabels = z.infer; export const TopicDtoSchema = z.object({ id: z.number().optional().nullable(), - createdAt: z.string().optional().nullable(), - updatedAt: z.string().optional().nullable(), + createdAt: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), + updatedAt: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), name: z.string().optional().nullable(), labels: TopicLabelsSchema.optional().nullable(), }); @@ -1308,11 +1324,19 @@ export const WikipediaSearchParamsSchema = z.object({ /** * 'wikiRevisionFrom' filter, will search pages modified after the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Date time in ISO format, ie. 2024-01-01T00:00:00. */ - wikiRevisionFrom: z.string().optional().nullable(), + wikiRevisionFrom: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), /** * 'wikiRevisionFrom' filter, will search pages modified before the specified date, the date could be passed as ISO or 'yyyy-mm-dd'. Date time in ISO format, ie. 2024-01-01T00:00:00. */ - wikiRevisionTo: z.string().optional().nullable(), + wikiRevisionTo: z + .union([z.string().date(), z.string().datetime()]) + .transform((val) => new Date(val)) + .optional() + .nullable(), /** * 'pageviewsFrom' filter, will search pages with at least the provided number of views per day. */ diff --git a/src/runtime.ts b/src/runtime.ts index ed7b124..a5c6a64 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -1,15 +1,3 @@ -/** - * Perigon API - * The Perigon API provides access to comprehensive news and web content data. To use the API, simply sign up for a Perigon Business Solutions account to obtain your API key. Your available features may vary based on your plan. See the Authentication section for details on how to use your API key. - * - * The version of the OpenAPI document: 1.0.0 - * Contact: data@perigon.io - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - export const BASE_PATH = "https://api.perigon.io".replace(/\/+$/, ""); export interface ConfigurationParameters { @@ -61,6 +49,116 @@ export class Configuration { export const DefaultConfig = new Configuration(); +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor( + public cause: Error, + msg?: string, + ) { + super(msg); + } +} + +export class HttpError extends Error { + public readonly status: number; + public readonly statusText: string; + public readonly response: Response; + public readonly body?: any; + + constructor(response: Response, body?: any) { + const message = `HTTP ${response.status}: ${response.statusText}`; + super(message); + this.name = "HttpError"; + this.status = response.status; + this.statusText = response.statusText; + this.response = response; + this.body = body; + } +} + +export class BadRequestError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = "BadRequestError"; + this.message = + body?.message || "Bad request - please check your input parameters"; + } +} + +export class UnauthorizedError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = "UnauthorizedError"; + this.message = "Authentication required - please check your credentials"; + } +} + +export class ForbiddenError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = "ForbiddenError"; + this.message = "Access denied - insufficient permissions"; + } +} + +export class NotFoundError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = "NotFoundError"; + this.message = body?.message || "Resource not found"; + } +} + +export class RateLimitError extends HttpError { + public readonly retryAfter?: number; + + constructor(response: Response, body?: any) { + super(response, body); + this.name = "RateLimitError"; + this.message = "Rate limit exceeded - please try again later"; + + // Extract retry-after header if present + const retryAfter = response.headers.get("retry-after"); + if (retryAfter) { + this.retryAfter = parseInt(retryAfter, 10); + } + } +} + +export class ServerError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = "ServerError"; + this.message = "Server error - please try again later"; + } +} + +export class NetworkError extends Error { + constructor(originalError: Error) { + super("Network error - please check your connection"); + this.name = "NetworkError"; + this.cause = originalError; + } +} + +export function createHttpError(response: Response, body?: any): HttpError { + if (response.status === 400) { + return new BadRequestError(response, body); + } else if (response.status === 401) { + return new UnauthorizedError(response, body); + } else if (response.status === 403) { + return new ForbiddenError(response, body); + } else if (response.status === 404) { + return new NotFoundError(response, body); + } else if (response.status === 429) { + return new RateLimitError(response, body); + } else if (response.status > 499) { + return new ServerError(response, body); + } else { + return new HttpError(response, body); + } +} + /** * This is the base class for all generated API classes. */ @@ -123,7 +221,17 @@ export class BaseAPI { if (response && response.status >= 200 && response.status < 300) { return response; } - throw new ResponseError(response, "Response returned an error code"); + let body; + try { + const contentType = response.headers.get("content-type"); + if (contentType?.includes("application/json")) { + body = await response.json(); + } + } catch { + // If we can't parse, that's fine + } + + throw createHttpError(response, body); } private async createFetchParams( @@ -266,36 +374,6 @@ function isFormData(value: any): value is FormData { return typeof FormData !== "undefined" && value instanceof FormData; } -export class ResponseError extends Error { - override name: "ResponseError" = "ResponseError"; - constructor( - public response: Response, - msg?: string, - ) { - super(msg); - } -} - -export class FetchError extends Error { - override name: "FetchError" = "FetchError"; - constructor( - public cause: Error, - msg?: string, - ) { - super(msg); - } -} - -export class RequiredError extends Error { - override name: "RequiredError" = "RequiredError"; - constructor( - public field: string, - msg?: string, - ) { - super(msg); - } -} - export const COLLECTION_FORMATS = { csv: ",", ssv: " ", @@ -403,27 +481,6 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } -export function mapValues(data: any, fn: (item: any) => any) { - const result: { [key: string]: any } = {}; - for (const key of Object.keys(data)) { - result[key] = fn(data[key]); - } - return result; -} - -export function canConsumeForm(consumes: Consume[]): boolean { - for (const consume of consumes) { - if ("multipart/form-data" === consume.contentType) { - return true; - } - } - return false; -} - -export interface Consume { - contentType: string; -} - export interface RequestContext { fetch: FetchAPI; url: string; @@ -450,47 +507,3 @@ export interface Middleware { post?(context: ResponseContext): Promise; onError?(context: ErrorContext): Promise; } - -export interface ApiResponse { - raw: Response; - value(): Promise; -} - -export interface ResponseTransformer { - (json: any): T; -} - -export class JSONApiResponse { - constructor( - public raw: Response, - private transformer: ResponseTransformer = (jsonValue: any) => jsonValue, - ) {} - - async value(): Promise { - return this.transformer(await this.raw.json()); - } -} - -export class VoidApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return undefined; - } -} - -export class BlobApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.blob(); - } -} - -export class TextApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.text(); - } -} diff --git a/templates/apis.mustache b/templates/apis.mustache index 2c09a6f..8afc3bd 100644 --- a/templates/apis.mustache +++ b/templates/apis.mustache @@ -49,20 +49,11 @@ export const {{operationIdCamelCase}}HeaderSchema = z.object({ }); {{/headerParams.0}} -{{#formParams.0}} -export const {{operationIdCamelCase}}FormSchema = z.object({ -{{#formParams}} - {{>param}}, -{{/formParams}} -}); -{{/formParams.0}} - export const {{operationIdCamelCase}}RequestSchema = z.object({ {{#queryParams.0}}...{{operationIdCamelCase}}QuerySchema.shape,{{/queryParams.0}} {{#bodyParam}}...{{operationIdCamelCase}}BodySchema.shape,{{/bodyParam}} {{#pathParams.0}}...{{operationIdCamelCase}}PathSchema.shape,{{/pathParams.0}} {{#headerParams.0}}...{{operationIdCamelCase}}HeaderSchema.shape,{{/headerParams.0}} -{{#formParams.0}}...{{operationIdCamelCase}}FormSchema.shape{{/formParams.0}} }); export type {{operationIdCamelCase}}Request = z.input; @@ -88,216 +79,6 @@ export class {{classname}} extends runtime.BaseAPI { * @deprecated {{/isDeprecated}} */ - {{^useSingleRequestParameter}} - async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{#isNullable}} | null{{/isNullable}}{{/isEnum}}, {{/allParams}}initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{{{returnType}}}{{#returnType}}{{#isResponseOptional}} | null | undefined {{/isResponseOptional}}{{/returnType}}{{^returnType}}void{{/returnType}}> { - {{#allParams.0}} - const params = {{operationIdCamelCase}}RequestSchema.parse({ {{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}} }); - {{#queryParams.0}} - const queryParameters = {{operationIdCamelCase}}QuerySchema.parse(params); - {{/queryParams.0}} - {{/allParams.0}} - - const headerParameters: runtime.HTTPHeaders = {}; - {{#bodyParam}} - {{^consumes}} - headerParameters['Content-Type'] = 'application/json'; - {{/consumes}} - {{#consumes.0}} - headerParameters['Content-Type'] = '{{{mediaType}}}'; - {{/consumes.0}} - {{/bodyParam}} - {{#headerParams}} - {{#isArray}} - if (params.{{paramName}} !== undefined) { - headerParameters['{{baseName}}'] = {{#uniqueItems}}Array.from({{/uniqueItems}}params.{{paramName}}{{#uniqueItems}}){{/uniqueItems}}.join(runtime.COLLECTION_FORMATS["{{collectionFormat}}"]); - } - {{/isArray}} - {{^isArray}} - if (params.{{paramName}} !== undefined) { - headerParameters['{{baseName}}'] = String(params.{{paramName}}); - } - {{/isArray}} - {{/headerParams}} - - {{#authMethods}} - {{#isBasic}} - {{#isBasicBasic}} - if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { - headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); - } - {{/isBasicBasic}} - {{#isBasicBearer}} - if (this.configuration && this.configuration.accessToken) { - const token = this.configuration.accessToken; - const tokenString = await token("{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}]); - if (tokenString) { - headerParameters["Authorization"] = `Bearer ${tokenString}`; - } - } - {{/isBasicBearer}} - {{/isBasic}} - {{#isApiKey}} - {{#isKeyInHeader}} - if (this.configuration && this.configuration.apiKey) { - headerParameters["{{keyParamName}}"] = await this.configuration.apiKey("{{keyParamName}}"); - } - {{/isKeyInHeader}} - {{#isKeyInQuery}} - if (this.configuration && this.configuration.apiKey) { - queryParameters["{{keyParamName}}"] = await this.configuration.apiKey("{{keyParamName}}"); - } - {{/isKeyInQuery}} - {{/isApiKey}} - {{#isOAuth}} - if (this.configuration && this.configuration.accessToken) { - headerParameters["Authorization"] = await this.configuration.accessToken("{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}]); - } - {{/isOAuth}} - {{/authMethods}} - - {{#hasFormParams}} - const consumes: runtime.Consume[] = [ - {{#consumes}} - { contentType: '{{{mediaType}}}' }, - {{/consumes}} - ]; - const canConsumeForm = runtime.canConsumeForm(consumes); - let formParams: { append(param: string, value: any): any }; - let useForm = false; - {{#formParams}} - {{#isFile}} - useForm = canConsumeForm; - {{/isFile}} - {{/formParams}} - if (useForm) { - formParams = new FormData(); - } else { - formParams = new URLSearchParams(); - } - - {{#formParams}} - {{#isArray}} - if (params.{{paramName}} !== undefined) { - {{#isCollectionFormatMulti}} - params.{{paramName}}.forEach((element) => { - formParams.append('{{baseName}}{{#useSquareBracketsInArrayNames}}[]{{/useSquareBracketsInArrayNames}}', element as any); - }) - {{/isCollectionFormatMulti}} - {{^isCollectionFormatMulti}} - formParams.append('{{baseName}}{{#useSquareBracketsInArrayNames}}[]{{/useSquareBracketsInArrayNames}}', {{#uniqueItems}}Array.from({{/uniqueItems}}params.{{paramName}}{{#uniqueItems}}){{/uniqueItems}}.join(runtime.COLLECTION_FORMATS["{{collectionFormat}}"])); - {{/isCollectionFormatMulti}} - } - {{/isArray}} - {{^isArray}} - if (params.{{paramName}} !== undefined) { - {{#isDateTimeType}} - formParams.append('{{baseName}}', (params.{{paramName}} as any).toISOString()); - {{/isDateTimeType}} - {{^isDateTimeType}} - {{#isPrimitiveType}} - formParams.append('{{baseName}}', params.{{paramName}} as any); - {{/isPrimitiveType}} - {{^isPrimitiveType}} - {{#isEnumRef}} - formParams.append('{{baseName}}', params.{{paramName}} as any); - {{/isEnumRef}} - {{^isEnumRef}} - {{^withoutRuntimeChecks}} - formParams.append('{{baseName}}', new Blob([JSON.stringify(params.{{paramName}})], { type: "application/json", })); - {{/withoutRuntimeChecks}}{{#withoutRuntimeChecks}} - formParams.append('{{baseName}}', new Blob([JSON.stringify(params.{{paramName}})], { type: "application/json", })); - {{/withoutRuntimeChecks}} - {{/isEnumRef}} - {{/isPrimitiveType}} - {{/isDateTimeType}} - } - {{/isArray}} - {{/formParams}} - {{/hasFormParams}} - - const response = await this.request({ - path: `{{{path}}}`{{#pathParams}}.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String(params.{{paramName}}))){{/pathParams}}, - method: '{{httpMethod}}', - headers: headerParameters, - {{#queryParams.0}}query: queryParameters,{{/queryParams.0}} - {{#hasBodyParam}} - {{#bodyParam}} - {{#isContainer}} - {{^withoutRuntimeChecks}} - body: params.{{paramName}}{{#isArray}}{{/isArray}}, - {{/withoutRuntimeChecks}} - {{#withoutRuntimeChecks}} - body: params.{{paramName}}, - {{/withoutRuntimeChecks}} - {{/isContainer}} - {{^isContainer}} - {{^isPrimitiveType}} - body: params.{{paramName}}, {{/isPrimitiveType}} - {{#isPrimitiveType}} - body: params.{{paramName}} as any, - {{/isPrimitiveType}} - {{/isContainer}} - {{/bodyParam}} - {{/hasBodyParam}} - {{#hasFormParams}} - body: formParams, - {{/hasFormParams}} - }, initOverrides); - - {{#returnType}} - {{#isResponseFile}} - const apiResponse = new runtime.BlobApiResponse(response); - {{/isResponseFile}} - {{^isResponseFile}} - {{#returnTypeIsPrimitive}} - {{#isMap}} - const apiResponse = new runtime.JSONApiResponse(response); - {{/isMap}} - {{#isArray}} - const apiResponse = new runtime.JSONApiResponse(response); - {{/isArray}} - {{#returnSimpleType}} - const apiResponse = this.isJsonMime(response.headers.get('content-type')) - ? new runtime.JSONApiResponse<{{returnType}}>(response) - : new runtime.TextApiResponse(response) as any; - {{/returnSimpleType}} - {{/returnTypeIsPrimitive}} - {{^returnTypeIsPrimitive}} - {{#isArray}} - const apiResponse = new runtime.JSONApiResponse(response{{^withoutRuntimeChecks}}, (jsonValue) => {{#uniqueItems}}new Set({{/uniqueItems}}jsonValue.map({{returnBaseType}}FromJSON){{/withoutRuntimeChecks}}){{#uniqueItems}}){{/uniqueItems}}; - {{/isArray}} - {{^isArray}} - {{#isMap}} - const apiResponse = new runtime.JSONApiResponse(response{{^withoutRuntimeChecks}}, (jsonValue) => runtime.mapValues(jsonValue, {{returnBaseType}}FromJSON){{/withoutRuntimeChecks}}); - {{/isMap}} - {{^isMap}} - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => {{returnBaseType}}Schema.parse(jsonValue)); - {{/isMap}} - {{/isArray}} - {{/returnTypeIsPrimitive}} - {{/isResponseFile}} - {{#isResponseOptional}} - switch (response.status) { - {{#responses}} - {{#is2xx}} - case {{code}}: - return {{#dataType}}await apiResponse.value(){{/dataType}}{{^dataType}}null{{/dataType}}; - {{/is2xx}} - {{/responses}} - default: - return await apiResponse.value(); - } - {{/isResponseOptional}} - {{^isResponseOptional}} - return await apiResponse.value(); - {{/isResponseOptional}} - {{/returnType}} - {{^returnType}} - // void return type - {{/returnType}} - } - {{/useSingleRequestParameter}} - {{#useSingleRequestParameter}} async {{nickname}}({{#allParams.0}}requestParameters: {{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<{{{returnType}}}{{#returnType}}{{#isResponseOptional}} | null | undefined {{/isResponseOptional}}{{/returnType}}{{^returnType}}void{{/returnType}}> { {{#allParams.0}} const params = {{operationIdCamelCase}}RequestSchema.parse(requestParameters); @@ -330,11 +111,6 @@ export class {{classname}} extends runtime.BaseAPI { {{#authMethods}} {{#isBasic}} - {{#isBasicBasic}} - if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { - headerParameters["Authorization"] = "Basic " + btoa(this.configuration.username + ":" + this.configuration.password); - } - {{/isBasicBasic}} {{#isBasicBearer}} if (this.configuration && this.configuration.accessToken) { const token = this.configuration.accessToken; @@ -345,85 +121,8 @@ export class {{classname}} extends runtime.BaseAPI { } {{/isBasicBearer}} {{/isBasic}} - {{#isApiKey}} - {{#isKeyInHeader}} - if (this.configuration && this.configuration.apiKey) { - headerParameters["{{keyParamName}}"] = await this.configuration.apiKey("{{keyParamName}}"); - } - {{/isKeyInHeader}} - {{#isKeyInQuery}} - if (this.configuration && this.configuration.apiKey) { - queryParameters["{{keyParamName}}"] = await this.configuration.apiKey("{{keyParamName}}"); - } - {{/isKeyInQuery}} - {{/isApiKey}} - {{#isOAuth}} - if (this.configuration && this.configuration.accessToken) { - headerParameters["Authorization"] = await this.configuration.accessToken("{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}]); - } - {{/isOAuth}} {{/authMethods}} - {{#hasFormParams}} - const consumes: runtime.Consume[] = [ - {{#consumes}} - { contentType: '{{{mediaType}}}' }, - {{/consumes}} - ]; - const canConsumeForm = runtime.canConsumeForm(consumes); - let formParams: { append(param: string, value: any): any }; - let useForm = false; - {{#formParams}} - {{#isFile}} - useForm = canConsumeForm; - {{/isFile}} - {{/formParams}} - if (useForm) { - formParams = new FormData(); - } else { - formParams = new URLSearchParams(); - } - - {{#formParams}} - {{#isArray}} - if (params.{{paramName}} !== undefined) { - {{#isCollectionFormatMulti}} - params.{{paramName}}.forEach((element) => { - formParams.append('{{baseName}}{{#useSquareBracketsInArrayNames}}[]{{/useSquareBracketsInArrayNames}}', element as any); - }) - {{/isCollectionFormatMulti}} - {{^isCollectionFormatMulti}} - formParams.append('{{baseName}}{{#useSquareBracketsInArrayNames}}[]{{/useSquareBracketsInArrayNames}}', {{#uniqueItems}}Array.from({{/uniqueItems}}params.{{paramName}}{{#uniqueItems}}){{/uniqueItems}}.join(runtime.COLLECTION_FORMATS["{{collectionFormat}}"])); - {{/isCollectionFormatMulti}} - } - {{/isArray}} - {{^isArray}} - if (params.{{paramName}} !== undefined) { - {{#isDateTimeType}} - formParams.append('{{baseName}}', (params.{{paramName}} as any).toISOString()); - {{/isDateTimeType}} - {{^isDateTimeType}} - {{#isPrimitiveType}} - formParams.append('{{baseName}}', params.{{paramName}} as any); - {{/isPrimitiveType}} - {{^isPrimitiveType}} - {{#isEnumRef}} - formParams.append('{{baseName}}', params.{{paramName}} as any); - {{/isEnumRef}} - {{^isEnumRef}} - {{^withoutRuntimeChecks}} - formParams.append('{{baseName}}', new Blob([JSON.stringify(params.{{paramName}})], { type: "application/json", })); - {{/withoutRuntimeChecks}}{{#withoutRuntimeChecks}} - formParams.append('{{baseName}}', new Blob([JSON.stringify(params.{{paramName}})], { type: "application/json", })); - {{/withoutRuntimeChecks}} - {{/isEnumRef}} - {{/isPrimitiveType}} - {{/isDateTimeType}} - } - {{/isArray}} - {{/formParams}} - {{/hasFormParams}} - const response = await this.request({ path: `{{{path}}}`{{#pathParams}}.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String(params.{{paramName}}))){{/pathParams}}, method: '{{httpMethod}}', @@ -448,65 +147,16 @@ export class {{classname}} extends runtime.BaseAPI { {{/isContainer}} {{/bodyParam}} {{/hasBodyParam}} - {{#hasFormParams}} - body: formParams, - {{/hasFormParams}} }, initOverrides); {{#returnType}} - {{#isResponseFile}} - const apiResponse = new runtime.BlobApiResponse(response); - {{/isResponseFile}} - {{^isResponseFile}} - {{#returnTypeIsPrimitive}} - {{#isMap}} - const apiResponse = new runtime.JSONApiResponse(response); - {{/isMap}} - {{#isArray}} - const apiResponse = new runtime.JSONApiResponse(response); - {{/isArray}} - {{#returnSimpleType}} - const apiResponse = this.isJsonMime(response.headers.get('content-type')) - ? new runtime.JSONApiResponse<{{returnType}}>(response) - : new runtime.TextApiResponse(response) as any; - {{/returnSimpleType}} - {{/returnTypeIsPrimitive}} - {{^returnTypeIsPrimitive}} - {{#isArray}} - const apiResponse = new runtime.JSONApiResponse(response{{^withoutRuntimeChecks}}, (jsonValue) => {{#uniqueItems}}new Set({{/uniqueItems}}jsonValue.map({{returnBaseType}}FromJSON){{/withoutRuntimeChecks}}){{#uniqueItems}}){{/uniqueItems}}; - {{/isArray}} - {{^isArray}} - {{#isMap}} - const apiResponse = new runtime.JSONApiResponse(response{{^withoutRuntimeChecks}}, (jsonValue) => runtime.mapValues(jsonValue, {{returnBaseType}}FromJSON){{/withoutRuntimeChecks}}); - {{/isMap}} - {{^isMap}} - const apiResponse = new runtime.JSONApiResponse(response, (jsonValue) => {{returnBaseType}}Schema.parse(jsonValue)); - {{/isMap}} - {{/isArray}} - {{/returnTypeIsPrimitive}} - {{/isResponseFile}} - {{#isResponseOptional}} - switch (response.status) { - {{#responses}} - {{#is2xx}} - case {{code}}: - return {{#dataType}}await apiResponse.value(){{/dataType}}{{^dataType}}null{{/dataType}}; - {{/is2xx}} - {{/responses}} - default: - return await apiResponse.value(); - } - {{/isResponseOptional}} - {{^isResponseOptional}} - return await apiResponse.value(); - {{/isResponseOptional}} + const raw = await response.json() + return {{returnBaseType}}Schema.parse(raw); {{/returnType}} {{^returnType}} // void return type {{/returnType}} } - {{/useSingleRequestParameter}} - {{/operation}} } {{/operations}} diff --git a/templates/modelZodOneOf.mustache b/templates/modelZodOneOf.mustache index a9fdd76..b9d0102 100644 --- a/templates/modelZodOneOf.mustache +++ b/templates/modelZodOneOf.mustache @@ -14,10 +14,33 @@ export const {{classname}}Schema = z.discriminatedUnion('{{discriminator.propert {{/discriminator}} {{^discriminator}} export const {{classname}}Schema = z.union([ -{{#oneOf}} - {{#oneOfModels}}z.lazy(() => {{.}}Schema){{/oneOfModels}}{{#oneOfArrays}}z.array(z.lazy(() => {{.}}Schema)){{/oneOfArrays}}{{#oneOfPrimitives}}{{#isArray}}z.array({{#items}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.string(){{/isDateTimeType}}{{#isDateType}}z.string().date(){{/isDateType}}{{/items}}){{/isArray}}{{^isArray}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.string(){{/isDateTimeType}}{{#isDateType}}z.string().date(){{/isDateType}}{{/isArray}}{{/oneOfPrimitives}}{{^-last}},{{/-last}} -{{/oneOf}} + {{#oneOf}} + {{#oneOfModels}} + z.lazy(() => {{.}}Schema){{/oneOfModels}} + {{#oneOfArrays}} + z.array(z.lazy(() => {{.}}Schema)) + {{/oneOfArrays}} + {{#oneOfPrimitives}} + {{#isArray}} + z.array( + {{#items}} + {{#isString}}z.string(){{/isString}} + {{#isNumeric}}z.number(){{/isNumeric}} + {{#isBoolean}}z.boolean(){{/isBoolean}} + {{#isDateTimeType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}}{{/isDateTimeType}} + {{#isDateType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}} + {{/items}}) + {{/isArray}} + {{^isArray}} + {{#isString}}z.string(){{/isString}} + {{#isNumeric}}z.number(){{/isNumeric}} + {{#isBoolean}}z.boolean(){{/isBoolean}} + {{#isDateTimeType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}}{{/isDateTimeType}} + {{#isDateType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}}{{/isDateType}} + {{/isArray}} + {{/oneOfPrimitives}}{{^-last}},{{/-last}} + {{/oneOf}} ]); {{/discriminator}} -export type {{classname}} = z.infer; \ No newline at end of file +export type {{classname}} = z.infer; diff --git a/templates/runtime.mustache b/templates/runtime.mustache index 40eadf6..f9beac7 100644 --- a/templates/runtime.mustache +++ b/templates/runtime.mustache @@ -1,5 +1,3 @@ - {{>licenseInfo}} - export const BASE_PATH = "{{{basePath}}}".replace(/\/+$/, ""); export interface ConfigurationParameters { @@ -66,6 +64,113 @@ export const DefaultConfig = new Configuration(); +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + } +} + +export class HttpError extends Error { + public readonly status: number; + public readonly statusText: string; + public readonly response: Response; + public readonly body?: any; + + constructor(response: Response, body?: any) { + const message = `HTTP ${response.status}: ${response.statusText}`; + super(message); + this.name = 'HttpError'; + this.status = response.status; + this.statusText = response.statusText; + this.response = response; + this.body = body; + } +} + +export class BadRequestError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'BadRequestError'; + this.message = body?.message || 'Bad request - please check your input parameters'; + } +} + +export class UnauthorizedError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'UnauthorizedError'; + this.message = 'Authentication required - please check your credentials'; + } +} + +export class ForbiddenError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'ForbiddenError'; + this.message = 'Access denied - insufficient permissions'; + } +} + +export class NotFoundError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'NotFoundError'; + this.message = body?.message || 'Resource not found'; + } +} + +export class RateLimitError extends HttpError { + public readonly retryAfter?: number; + + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'RateLimitError'; + this.message = 'Rate limit exceeded - please try again later'; + + // Extract retry-after header if present + const retryAfter = response.headers.get('retry-after'); + if (retryAfter) { + this.retryAfter = parseInt(retryAfter, 10); + } + } +} + +export class ServerError extends HttpError { + constructor(response: Response, body?: any) { + super(response, body); + this.name = 'ServerError'; + this.message = 'Server error - please try again later'; + } +} + +export class NetworkError extends Error { + constructor(originalError: Error) { + super('Network error - please check your connection'); + this.name = 'NetworkError'; + this.cause = originalError; + } +} + +export function createHttpError(response: Response, body?: any): HttpError { + if (response.status === 400) { + return new BadRequestError(response, body); + } else if (response.status === 401) { + return new UnauthorizedError(response, body); + } else if (response.status === 403) { + return new ForbiddenError(response, body); + } else if (response.status === 404) { + return new NotFoundError(response, body); + } else if (response.status === 429) { + return new RateLimitError(response, body); + } else if (response.status > 499) { + return new ServerError(response, body) + } else { + return new HttpError(response, body); + } +} + + /** * This is the base class for all generated API classes. */ @@ -117,7 +222,17 @@ export class BaseAPI { if (response && (response.status >= 200 && response.status < 300)) { return response; } - throw new ResponseError(response, 'Response returned an error code'); + let body; + try { + const contentType = response.headers.get('content-type'); + if (contentType?.includes('application/json')) { + body = await response.json(); + } + } catch { + // If we can't parse, that's fine + } + + throw createHttpError(response, body); } private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { @@ -237,26 +352,6 @@ function isFormData(value: any): value is FormData { return typeof FormData !== "undefined" && value instanceof FormData; } -export class ResponseError extends Error { - override name: "ResponseError" = "ResponseError"; - constructor(public response: Response, msg?: string) { - super(msg); - } -} - -export class FetchError extends Error { - override name: "FetchError" = "FetchError"; - constructor(public cause: Error, msg?: string) { - super(msg); - } -} - -export class RequiredError extends Error { - override name: "RequiredError" = "RequiredError"; - constructor(public field: string, msg?: string) { - super(msg); - } -} export const COLLECTION_FORMATS = { csv: ",", @@ -322,31 +417,6 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } - - -{{^withoutRuntimeChecks}} -export function mapValues(data: any, fn: (item: any) => any) { - const result: { [key: string]: any } = {}; - for (const key of Object.keys(data)) { - result[key] = fn(data[key]); - } - return result; -} -{{/withoutRuntimeChecks}} - -export function canConsumeForm(consumes: Consume[]): boolean { - for (const consume of consumes) { - if ('multipart/form-data' === consume.contentType) { - return true; - } - } - return false; -} - -export interface Consume { - contentType: string; -} - export interface RequestContext { fetch: FetchAPI; url: string; @@ -374,43 +444,5 @@ export interface Middleware { onError?(context: ErrorContext): Promise; } -export interface ApiResponse { - raw: Response; - value(): Promise; -} - -export interface ResponseTransformer { - (json: any): T; -} - -export class JSONApiResponse { - constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} - - async value(): Promise { - return this.transformer(await this.raw.json()); - } -} -export class VoidApiResponse { - constructor(public raw: Response) {} - async value(): Promise { - return undefined; - } -} - -export class BlobApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.blob(); - }; -} - -export class TextApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.text(); - }; -} diff --git a/templates/zodFieldType.mustache b/templates/zodFieldType.mustache index 2f3b4ce..665457a 100644 --- a/templates/zodFieldType.mustache +++ b/templates/zodFieldType.mustache @@ -1 +1 @@ -{{#isEnum}}z.enum([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]){{/isEnum}}{{^isEnum}}{{#isContainer}}{{#isArray}}z.array({{>zodArrayItemType}}){{/isArray}}{{#isMap}}z.record(z.string(), {{>zodMapValueType}}){{/isMap}}{{/isContainer}}{{^isContainer}}{{#isPrimitiveType}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.string(){{/isDateTimeType}}{{#isDateType}}z.string().date(){{/isDateType}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isModel}}{{#dataType}}{{dataType}}Schema{{/dataType}}{{/isModel}}{{^isModel}}{{dataType}}Schema{{/isModel}}{{/isPrimitiveType}}{{/isContainer}}{{/isEnum}} \ No newline at end of file +{{#isEnum}}z.enum([{{#allowableValues}}{{#enumVars}}{{{value}}}{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]){{/isEnum}}{{^isEnum}}{{#isContainer}}{{#isArray}}z.array({{>zodArrayItemType}}){{/isArray}}{{#isMap}}z.record(z.string(), {{>zodMapValueType}}){{/isMap}}{{/isContainer}}{{^isContainer}}{{#isPrimitiveType}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateTimeType}}{{#isDateType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isModel}}{{#dataType}}{{dataType}}Schema{{/dataType}}{{/isModel}}{{^isModel}}{{dataType}}Schema{{/isModel}}{{/isPrimitiveType}}{{/isContainer}}{{/isEnum}} diff --git a/templates/zodMapValueType.mustache b/templates/zodMapValueType.mustache index 6d4a920..f088391 100644 --- a/templates/zodMapValueType.mustache +++ b/templates/zodMapValueType.mustache @@ -1 +1 @@ -{{#items}}{{#isContainer}}{{#isArray}}z.array({{>zodArrayItemType}}){{/isArray}}{{/isContainer}}{{^isContainer}}{{#isPrimitiveType}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.string(){{/isDateTimeType}}{{#isDateType}}z.string().date(){{/isDateType}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}z.enum([{{#allowableValues}}{{#enumVars}}'{{{value}}}'{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]){{/isEnum}}{{^isEnum}}{{#isModel}}{{#dataType}}{{dataType}}Schema{{/dataType}}{{/isModel}}{{^isModel}}{{dataType}}Schema{{/isModel}}{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{/items}} \ No newline at end of file +{{#items}}{{#isContainer}}{{#isArray}}z.array({{>zodArrayItemType}}){{/isArray}}{{/isContainer}}{{^isContainer}}{{#isPrimitiveType}}{{#isString}}z.string(){{/isString}}{{#isNumeric}}z.number(){{/isNumeric}}{{#isBoolean}}z.boolean(){{/isBoolean}}{{#isDateTimeType}}z.string(){{/isDateTimeType}}{{#isDateType}}z.union([z.string().date(),z.string().datetime()]).transform(val => new Date(val)){{/isDateType}}{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}z.enum([{{#allowableValues}}{{#enumVars}}'{{{value}}}'{{^-last}}, {{/-last}}{{/enumVars}}{{/allowableValues}}]){{/isEnum}}{{^isEnum}}{{#isModel}}{{#dataType}}{{dataType}}Schema{{/dataType}}{{/isModel}}{{^isModel}}{{dataType}}Schema{{/isModel}}{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{/items}} diff --git a/tests/integration/v1api-errors.test.ts b/tests/integration/v1api-errors.test.ts index d6d037b..c9e8417 100644 --- a/tests/integration/v1api-errors.test.ts +++ b/tests/integration/v1api-errors.test.ts @@ -1,14 +1,17 @@ import { V1Api, Configuration } from "../../src"; import { - ResponseError, - FetchError, - RequiredError, Middleware, + UnauthorizedError, + BadRequestError, + ForbiddenError, + NotFoundError, + RateLimitError, + ServerError, + HttpError, } from "../../src/runtime"; import { ZodError } from "zod"; import * as dotenv from "dotenv"; -// Load environment variables from .env file dotenv.config(); describe("Perigon SDK Error Handling and Logging Tests", () => { @@ -19,7 +22,6 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { let loggedMessages: any[]; beforeAll(() => { - // Mock console methods to capture logs loggedErrors = []; loggedMessages = []; originalConsoleError = console.error; @@ -37,90 +39,162 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { }); afterAll(() => { - // Restore original console methods console.error = originalConsoleError; console.log = originalConsoleLog; }); beforeEach(() => { - // Clear logged messages before each test loggedErrors.length = 0; loggedMessages.length = 0; }); - describe("RequiredError handling", () => { + const createMockFetch = (status: number) => { + return async (url: string, init?: RequestInit): Promise => { + return new Response(`Mock ${status} error`, { + status, + headers: status === 429 ? { "retry-after": "30" } : {}, + }); + }; + }; + + describe("Error handling", () => { beforeEach(() => { - // Create API instance with valid configuration const configuration = new Configuration({ apiKey: () => Promise.resolve("test-api-key"), }); api = new V1Api(configuration); }); - it("should throw RequiredError when required parameter is missing", async () => { + it("should throw ZodError when required parameter is missing", async () => { try { - // Call method that requires 'id' parameter without providing it await api.getJournalistById({ id: null as any }); - fail("Expected RequiredError to be thrown"); + fail("Expected ZodError to be thrown"); } catch (error) { expect(error).toBeInstanceOf(ZodError); - const zodError = error as ZodError; - expect(zodError.name).toBe("ZodError"); - expect(zodError.issues[0].path).toContain("id"); - expect(zodError.issues[0].message).toContain( - "Expected string, received null", - ); } }); }); - describe("ResponseError handling", () => { - beforeEach(() => { - // Create API instance with invalid API key to trigger 401/403 errors + describe("HTTP Error handling", () => { + it("should throw BadRequestError for 400 status", async () => { const configuration = new Configuration({ - apiKey: () => Promise.resolve("invalid-api-key"), + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(400), }); api = new V1Api(configuration); + + try { + await api.searchArticles({ q: "test" }); + fail("Expected BadRequestError to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(BadRequestError); + } }); - it("should throw ResponseError for 401 Unauthorized", async () => { + it("should throw UnauthorizedError for 401 status", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(401), + }); + api = new V1Api(configuration); + try { - await api.searchArticles({ q: "test", size: 1 }); - fail("Expected ResponseError to be thrown"); + await api.searchArticles({ q: "test" }); + fail("Expected UnauthorizedError to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(ResponseError); - const responseError = error as ResponseError; - expect(responseError.name).toBe("ResponseError"); - expect(responseError.response).toBeDefined(); - expect(responseError.response.status).toBe(401); - expect(responseError.message).toBe("Response returned an error code"); + expect(error).toBeInstanceOf(UnauthorizedError); } - }, 15000); + }); + + it("should throw ForbiddenError for 403 status", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(403), + }); + api = new V1Api(configuration); - it("should throw ResponseError for 403 Forbidden", async () => { try { - await api.searchCompanies({ name: "test", size: 1 }); - fail("Expected ResponseError to be thrown"); + await api.searchArticles({ q: "test" }); + fail("Expected ForbiddenError to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(ResponseError); - const responseError = error as ResponseError; - expect(responseError.name).toBe("ResponseError"); - expect(responseError.response).toBeDefined(); - expect([401, 403]).toContain(responseError.response.status); + expect(error).toBeInstanceOf(ForbiddenError); } - }, 15000); + }); + + it("should throw NotFoundError for 404 status", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(404), + }); + api = new V1Api(configuration); + + try { + await api.getJournalistById({ id: "non-existent" }); + fail("Expected NotFoundError to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(NotFoundError); + } + }); + + it("should throw RateLimitError for 429 status", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(429), + }); + api = new V1Api(configuration); + + try { + await api.searchArticles({ q: "test" }); + fail("Expected RateLimitError to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(RateLimitError); + } + }); + + it("should throw ServerError for 500 status", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(500), + }); + api = new V1Api(configuration); + + try { + await api.searchArticles({ q: "test" }); + fail("Expected ServerError to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ServerError); + } + }); + + it("should throw generic HttpError for unhandled status codes", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("test-api-key"), + fetchApi: createMockFetch(418), + }); + api = new V1Api(configuration); - it("should provide access to response details in ResponseError", async () => { try { await api.searchArticles({ q: "test" }); - fail("Expected ResponseError to be thrown"); + fail("Expected HttpError to be thrown"); + } catch (error) { + expect(error).toBeInstanceOf(HttpError); + expect(error).not.toBeInstanceOf(BadRequestError); + } + }); + }); + + describe("Real API Error handling", () => { + it("should throw UnauthorizedError with invalid API key", async () => { + const configuration = new Configuration({ + apiKey: () => Promise.resolve("invalid-api-key"), + }); + api = new V1Api(configuration); + + try { + await api.searchArticles({ q: "test", size: 1 }); + fail("Expected UnauthorizedError to be thrown"); } catch (error) { - expect(error).toBeInstanceOf(ResponseError); - const responseError = error as ResponseError; - expect(responseError.response).toBeDefined(); - expect(responseError.response.status).toBeDefined(); - expect(responseError.response.headers).toBeDefined(); - expect(typeof responseError.response.status).toBe("number"); + expect(error).toBeInstanceOf(UnauthorizedError); } }, 15000); }); @@ -131,7 +205,6 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { beforeEach(() => { requestLogs = []; - // Create logging middleware const loggingMiddleware: Middleware = { pre: async (context) => { requestLogs.push({ @@ -154,7 +227,6 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { }, }; - // Create API with invalid key and logging middleware const configuration = new Configuration({ apiKey: () => Promise.resolve("invalid-api-key"), middleware: [loggingMiddleware], @@ -167,12 +239,10 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { await api.searchArticles({ q: "test", size: 1 }); fail("Expected error to be thrown"); } catch (error) { - // Verify request was logged expect(requestLogs).toHaveLength(1); expect(requestLogs[0].url).toContain("/v1/articles/all"); expect(requestLogs[0].method).toBe("GET"); - // Verify console logging expect(loggedMessages.length).toBeGreaterThan(0); expect( loggedMessages.some((msg) => @@ -186,37 +256,4 @@ describe("Perigon SDK Error Handling and Logging Tests", () => { } }, 15000); }); - - describe("Error information preservation", () => { - it("should preserve original error information in wrapped errors", () => { - const requiredError = new RequiredError("testField", "Test message"); - expect(requiredError.field).toBe("testField"); - expect(requiredError.message).toBe("Test message"); - expect(requiredError.name).toBe("RequiredError"); - }); - - it("should preserve cause in FetchError", () => { - const originalError = new Error("Original network error"); - const fetchError = new FetchError(originalError, "Fetch failed"); - - expect(fetchError.cause).toBe(originalError); - expect(fetchError.message).toBe("Fetch failed"); - expect(fetchError.name).toBe("FetchError"); - }); - - it("should preserve response in ResponseError", async () => { - const mockResponse = new Response("Error content", { - status: 404, - statusText: "Not Found", - }); - - const responseError = new ResponseError(mockResponse, "Request failed"); - - expect(responseError.response).toBe(mockResponse); - expect(responseError.response.status).toBe(404); - expect(responseError.response.statusText).toBe("Not Found"); - expect(responseError.message).toBe("Request failed"); - expect(responseError.name).toBe("ResponseError"); - }); - }); });