diff --git a/src/lib/error.ts b/src/lib/error.ts index c8d196e..24de6bc 100644 --- a/src/lib/error.ts +++ b/src/lib/error.ts @@ -7,21 +7,105 @@ export interface GraphError { } /** RawError is an error that can be converted to a string by ErrorMessage */ -export type RawError = Error | string | GraphError[]; +export type RawError = ErrorMessage | Error | string | GraphError[]; +/** + * Type guard to check if an error is a RawError + * @param error The error to check + * @returns true if the error is a RawError, false otherwise + */ +export const isRawError = (error: unknown): error is RawError => { + return ( + error instanceof ErrorMessage || + error instanceof Error || + typeof error === 'string' || + (Array.isArray(error) && + error.every( + (e) => typeof e.message === 'string' && (e.path === undefined || Array.isArray(e.path)) + )) + ); +}; + +/** + * Converts any error (including GraphQL errors, standard Errors, strings, and + * ErrorMessages) into a consistent string format for display. + * @param error The error to convert to a string. + * @returns A string representation of the error, suitable for display to users. + */ +export const catchErrorString = (error: unknown): string => { + if (error instanceof ErrorMessage) { + return error.toString(); + } else if (isRawError(error)) { + return new ErrorMessage(error).toString(); + } else { + return String(error); + } +}; + +/** + * Checks a typical response from a GraphQL server for error and missing data, + * throwing an appropriate ErrorMessage if an error is found or if no data is + * returned. + * @param resourceName A human-readable name for the resource being fetched, + * used in error messages. + * @param response The response from the GraphQL server, which may contain an + * 'errors' array and a 'data' field. + * @throws An ErrorMessage if the response contains errors or if the data field + * is missing (undefined or null). + */ +export const checkGraphResponse = ( + resourceName: string, + response: { errors?: GraphError[] | null; data?: unknown } +): void => { + if (response.errors && response.errors.length > 0) { + throw new ErrorMessage(`Error fetching ${resourceName}`, response.errors); + } + if (!response.data) { + throw new ErrorMessage(`No data returned for ${resourceName}`); + } +}; + +/** + * A class that represents an error message, which can be constructed from various types of raw errors + * and provides methods to convert those errors into a consistent format (lines of text) for display. + * It also supports wrapping existing error messages with additional context. + */ export class ErrorMessage { private _lines: string[] = []; /** - * Converts a RawError to an array of lines and stores it for later access, - * or initializes without any errors if the input is null or undefined. - * @param raw The raw error to convert and store, or null/undefined for no error. - * @throws If the raw error is of an unsupported type. + * Always creates a new ErrorMessage instance, even if there are no errors. + * @param errors The raw errors to convert and store, or null/undefined for no error. + * @throws If any of the raw errors are of an unsupported type. */ - constructor(raw: RawError | null | undefined) { - if (raw) { - this._lines = ErrorMessage.rawErrorToLines(raw); - } + constructor(...errors: (RawError | null | undefined)[]) { + if (errors.length === 0) return; + this._lines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e)); + } + + /** + * Creates a new ErrorMessage only if the provided errors are not null or + * undefined. If no errors are provided, returns null. + */ + static from(...errors: (RawError | null | undefined)[]): ErrorMessage | null { + if (errors.length === 0) return null; + return new ErrorMessage(...errors); + } + + /** + * Wraps this ErrorMessage inside another error, nesting the original error. + * @param errors The raw errors to wrap around this error, or null/undefined for no additional error. + * @returns A new ErrorMessage instance that includes the original error and any new errors. + */ + wrap(...errors: (RawError | null | undefined)[]): ErrorMessage { + if (errors.length === 0) return this; + const newLines = errors.flatMap((e) => ErrorMessage.rawErrorToLines(e)); + return new ErrorMessage(...newLines, ...this._lines); + } + + /** returns true if there are any error lines */ + hasError(): boolean { + return this._lines.length > 0; } /** returns the stored lines */ @@ -37,28 +121,14 @@ export class ErrorMessage { return this._lines.join('
'); } - /** returns true if there are any error lines */ - hasError(): boolean { - return this._lines.length > 0; - } - - /** adds a new line to the error message */ - addLine(line: string): void { - this._lines.push(line); - } - - /** optionally returns a new ErrorMessage only if the RawError is not empty */ - static from(raw: RawError | null | undefined): ErrorMessage | null { - if (!raw) return null; - return new ErrorMessage(raw); - } - /** converts a RawError to an array of lines */ static rawErrorToLines(raw: RawError | null | undefined): string[] { - if (!raw) return ['No error']; + if (!raw) return []; let errorLines: string[]; - if (typeof raw === 'string') { + if (raw instanceof ErrorMessage) { + errorLines = raw.lines; + } else if (typeof raw === 'string') { errorLines = [raw]; } else if (raw instanceof Error) { errorLines = [raw.message]; diff --git a/src/lib/index.ts b/src/lib/index.ts index a286392..c3dd976 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -67,7 +67,14 @@ export { ToolbarGroup, Toolbar } from './Toolbar'; -export { type GraphError, type RawError, ErrorMessage } from './error'; +export { + type GraphError, + type RawError, + isRawError, + catchErrorString, + checkGraphResponse, + ErrorMessage +} from './error'; export { NavigationItem, NavigationManager,