From 290d9a2cc988ec14f196a01fbb81cd7a08ae36b0 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Sun, 6 Sep 2026 00:48:04 +0200 Subject: [PATCH] refactor(client): expose error hierarchy via src/client/errors.ts and add code property --- src/client/api-client.ts | 59 +++++++++++++++++++++++++++++----------- src/client/errors.ts | 15 ++++++++++ src/client/index.ts | 2 ++ 3 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 src/client/errors.ts diff --git a/src/client/api-client.ts b/src/client/api-client.ts index 02ccec7..1e9191a 100644 --- a/src/client/api-client.ts +++ b/src/client/api-client.ts @@ -19,6 +19,7 @@ export class OpenProjectError extends Error { readonly statusCode?: number; readonly errorIdentifier?: string; readonly details?: unknown; + readonly code: string; constructor( message: string, @@ -27,6 +28,7 @@ export class OpenProjectError extends Error { errorIdentifier?: string; details?: unknown; cause?: unknown; + code?: string; } ) { super(message); @@ -34,6 +36,7 @@ export class OpenProjectError extends Error { this.statusCode = options?.statusCode; this.errorIdentifier = options?.errorIdentifier; this.details = options?.details; + this.code = options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_ERROR"; if (options?.cause) { this.cause = options.cause; } @@ -41,36 +44,51 @@ export class OpenProjectError extends Error { } export class OpenProjectAuthenticationError extends OpenProjectError { - constructor(message = "Authentication failed. Please check your OpenProject API key.", options?: { errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 401, ...options }); + constructor( + message = "Authentication failed. Please check your OpenProject API key.", + options?: { errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 401, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_AUTH_ERROR", ...options }); this.name = "OpenProjectAuthenticationError"; } } export class OpenProjectForbiddenError extends OpenProjectError { - constructor(message = "Access forbidden. You do not have permission to access this resource.", options?: { errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 403, ...options }); + constructor( + message = "Access forbidden. You do not have permission to access this resource.", + options?: { errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 403, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_FORBIDDEN", ...options }); this.name = "OpenProjectForbiddenError"; } } export class OpenProjectNotFoundError extends OpenProjectError { - constructor(message = "Resource not found.", options?: { errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 404, ...options }); + constructor( + message = "Resource not found.", + options?: { errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 404, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_NOT_FOUND", ...options }); this.name = "OpenProjectNotFoundError"; } } export class OpenProjectConflictError extends OpenProjectError { - constructor(message = "Conflict updating resource. Lock version mismatch.", options?: { errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 409, ...options }); + constructor( + message = "Conflict updating resource. Lock version mismatch.", + options?: { errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 409, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_CONFLICT", ...options }); this.name = "OpenProjectConflictError"; } } export class OpenProjectValidationError extends OpenProjectError { - constructor(message = "Validation failed for request data.", options?: { errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 422, ...options }); + constructor( + message = "Validation failed for request data.", + options?: { errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 422, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_VALIDATION_ERROR", ...options }); this.name = "OpenProjectValidationError"; } } @@ -78,23 +96,32 @@ export class OpenProjectValidationError extends OpenProjectError { export class OpenProjectRateLimitError extends OpenProjectError { readonly retryAfter?: number; - constructor(message = "Rate limit exceeded. Please wait before retrying.", options?: { retryAfter?: number; errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: 429, ...options }); + constructor( + message = "Rate limit exceeded. Please wait before retrying.", + options?: { retryAfter?: number; errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: 429, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_RATE_LIMIT", ...options }); this.name = "OpenProjectRateLimitError"; this.retryAfter = options?.retryAfter; } } export class OpenProjectServerError extends OpenProjectError { - constructor(message = "OpenProject internal server error.", options?: { statusCode?: number; errorIdentifier?: string; details?: unknown; cause?: unknown }) { - super(message, { statusCode: options?.statusCode ?? 500, ...options }); + constructor( + message = "OpenProject internal server error.", + options?: { statusCode?: number; errorIdentifier?: string; details?: unknown; cause?: unknown; code?: string } + ) { + super(message, { statusCode: options?.statusCode ?? 500, code: options?.code ?? options?.errorIdentifier ?? "OPENPROJECT_SERVER_ERROR", ...options }); this.name = "OpenProjectServerError"; } } export class OpenProjectNetworkError extends OpenProjectError { - constructor(message = "Network error connecting to OpenProject instance.", options?: { cause?: unknown }) { - super(message, options); + constructor( + message = "Network error connecting to OpenProject instance.", + options?: { cause?: unknown; code?: string } + ) { + super(message, { code: options?.code ?? "OPENPROJECT_NETWORK_ERROR", ...options }); this.name = "OpenProjectNetworkError"; } } diff --git a/src/client/errors.ts b/src/client/errors.ts new file mode 100644 index 0000000..290af1a --- /dev/null +++ b/src/client/errors.ts @@ -0,0 +1,15 @@ +/** + * OpenProject Client Errors + */ + +export { + OpenProjectError, + OpenProjectAuthenticationError, + OpenProjectForbiddenError, + OpenProjectNotFoundError, + OpenProjectConflictError, + OpenProjectValidationError, + OpenProjectRateLimitError, + OpenProjectServerError, + OpenProjectNetworkError, +} from "./api-client.ts"; diff --git a/src/client/index.ts b/src/client/index.ts index 4fb57ac..8adc56d 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,3 +2,5 @@ export * from "./types.ts"; export * from "./api-client.ts"; export * from "./hal-parser.ts"; export * from "./filter-builder.ts"; +export * from "./errors.ts"; +