Ctrl K
ComunidadGuías…

cli/errors.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import {
  AdminApiError,
  AdminApiSchemaError,
} from "#shared/admin-api-client.ts";

/** The failure kinds the CLI reports. Each kind maps to one exit code. */
export type CliErrorKind =
  | "usage"
  | "validation"
  | "not_allowed"
  | "not_found"
  | "server"
  | "io";

/** The exit code each kind answers with. */
export const EXIT_CODES: Record<CliErrorKind, number> = {
  io: 6,
  not_allowed: 4,
  not_found: 5,
  server: 6,
  usage: 2,
  validation: 3,
};

/** One problem with one body field. */
export type FieldProblem = { field: string; message: string };

/** The one error type the CLI raises. The message names the problem and the
 *  fix; `status` carries the HTTP status when the server answered, and
 *  `fields` names per-field problems when the body failed a check. */
export class CliError extends Error {
  readonly fields: FieldProblem[] | null;
  readonly kind: CliErrorKind;
  readonly status: number | undefined;

  constructor(
    kind: CliErrorKind,
    message: string,
    fields: FieldProblem[] | null = null,
    status?: number,
  ) {
    super(message);
    this.fields = fields;
    this.kind = kind;
    this.status = status;
  }
}

/** The JSON object stderr carries for one failure. An absent status means no
 *  server answered, and the contract renders it as null. */
export const errorJson = (error: CliError): unknown => ({
  error: {
    fields: error.fields,
    kind: error.kind,
    message: error.message,
    status: error.status ?? null,
  },
});

/** The exit code one mapped error answers with. */
export const exitCodeFor = (error: CliError): number => EXIT_CODES[error.kind];

/** The stderr line and exit code one failure reports: the contract's JSON
 *  envelope with its trailing newline. `fallbackKind` reaches the failures
 *  no other rule names. */
export const toErrorResult = (
  error: unknown,
  fallbackKind: CliErrorKind = "server",
): { code: number; err: string } => {
  const cli = cliErrorFromUnknown(error, fallbackKind);
  return { code: exitCodeFor(cli), err: `${JSON.stringify(errorJson(cli))}\n` };
};

/** Map one HTTP status to the CLI error kind the contract names. */
const kindForStatus = (status: number | undefined): CliErrorKind => {
  if (status === 400) return "validation";
  if (status === 401 || status === 403) return "not_allowed";
  if (status === 404) return "not_found";
  return "server";
};

/** The server's own error text when its body names one. */
const serverMessage = (error: AdminApiError): string => {
  const data = error.data;
  if (
    typeof data === "object" &&
    data !== null &&
    "error" in data &&
    typeof data.error === "string"
  ) {
    return data.error;
  }
  return error.message;
};

/** Map any thrown value to the contract's error shape. A `CliError` passes
 *  through; the typed client's refusal maps by status; a success answer the
 *  table's envelope rejects keeps the served status under the server kind;
 *  every other failure takes the caller's fallback kind — a transport
 *  failure for the api command, a local input or environment failure for the
 *  importer. */
export const cliErrorFromUnknown = (
  error: unknown,
  fallbackKind: CliErrorKind = "server",
): CliError => {
  if (error instanceof CliError) return error;
  if (error instanceof AdminApiError) {
    return new CliError(
      kindForStatus(error.status),
      serverMessage(error),
      null,
      error.status,
    );
  }
  if (error instanceof AdminApiSchemaError) {
    // The served status stays: the server did answer, and its status names
    // the malformed reply.
    return new CliError("server", error.message, null, error.status);
  }
  return new CliError(
    fallbackKind,
    error instanceof Error ? error.message : String(error),
  );
};

/** Map valibot's issues to the per-field problems the contract carries. */
export const fieldProblems = (
  issues: readonly {
    message: string;
    path?: readonly { key: unknown }[] | undefined;
  }[],
): FieldProblem[] =>
  issues.map((issue) => ({
    field: issue.path?.map((item) => String(item.key)).join(".") ?? "",
    message: issue.message,
  }));