@eleva-io/erp-sdk
Version:
SDK oficial para el ERP de Eleva
280 lines • 11.9 kB
JavaScript
import { z } from 'zod';
import { ERRORS_CATALOG, ERROR_CODES } from './catalog';
import { ErrorKind } from './catalog_types';
import { BASE_ERROR_CODES } from './catalog_base';
import { API_ERROR_CODES } from './catalog_api';
/**
* Permissive schema for parsing unknown error payloads. All fields except `code`
* are optional so malformed responses always produce a valid result.
* `errors` is the legacy name for `details` (kept for backwards compatibility).
*/
const errorSchema = z.object({
code: z.string(),
kind: z.string().optional(),
status: z.number().optional(),
message: z.string().optional(),
details: z.unknown().optional(),
/** @deprecated Use `details` instead */
errors: z.unknown().optional(),
});
/** Pre-computed set of all valid error code strings for O(1) membership checks. */
const KNOWN_CODES = new Set(Object.keys(ERROR_CODES));
/** Pre-computed set of all valid `ErrorKind` values for O(1) membership checks. */
const VALID_KINDS = new Set(Object.values(ErrorKind));
/**
* Looks up the catalog entry for `code` and builds an `ElevaError`.
* Module-private — use the static factory methods on `ElevaError` instead.
*/
const _newElevaErrorFromCode = (code, args) => {
const entry = ERRORS_CATALOG[code];
return new ElevaError({
code,
kind: entry.kind,
status: entry.status,
details: args.details ?? {},
message: args.message || entry.message,
cause: args.cause,
});
};
/**
* Factory for codes whose `details` are entirely optional (every field is `.partial()`).
* Returns a function with signature `(args?: { message?, details? }) => ElevaError<C>`.
*/
const _optionalFactory = (code) => {
return (args = {}) => _newElevaErrorFromCode(code, { details: args.details ?? {}, message: args.message });
};
/**
* Factory for codes where `details` is built from a single required string argument.
* The argument key mirrors the `details` key (e.g. `'entity'` → `{ entity }`).
*/
const _namedFieldFactory = (code, key) => {
return (args) => _newElevaErrorFromCode(code, { details: { [key]: args[key] }, message: args.message });
};
/**
* Factory for codes where `details` is required and passed through directly.
* Returns a function with signature `(args: { details, message? }) => ElevaError<C>`.
*/
const _requiredDetailsFactory = (code) => {
return (args) => _newElevaErrorFromCode(code, { details: args.details, message: args.message });
};
/**
* Central error class for the Eleva ERP SDK.
*
* Extends `Error` with structured metadata: `code`, `kind`, `status`, and typed `details`.
*
* ```ts
* // Create
* throw ElevaError.notFound({ entity: 'Invoice' })
* throw ElevaError.conflict({ details: [{ key: 'email', value: 'a@b.com', cause: 'duplicate' }] })
*
* // Narrow
* if (err instanceof ElevaError && err.is(ERROR_CODES.NOT_FOUND)) {
* console.log(err.details.entity) // string
* }
* ```
*
* Serialisation: `toJSON()` for logging, `toResponse()` for sanitised API output.
*/
export class ElevaError extends Error {
/** Error code constant identifying the failure condition. */
code;
/** `DOMAIN` (safe to expose) or `SYSTEM` (sanitised before sending to clients). */
kind;
/** HTTP status code for this error. */
status;
/** Structured, code-specific failure context. */
details;
constructor(args) {
super(args.message, { cause: args.cause });
this.name = 'ElevaError';
this.code = args.code;
this.kind = args.kind;
this.status = args.status;
this.details = args.details;
// Removes the constructor frame from V8 stack traces.
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
/**
* Type-guard: narrows to `ElevaError<K>` when `this.code === code`.
* After a positive check `this.details` is fully typed for `K`.
* @example
* if (err.is(ERROR_CODES.NOT_FOUND)) console.log(err.details.entity)
*/
is(code) {
return this.code === code;
}
/** Full serialised representation for logging / diagnostics. */
toJSON() {
return {
code: this.code,
kind: this.kind,
status: this.status,
message: this.message,
details: this.details,
};
}
toString() {
return JSON.stringify(this);
}
/**
* Sanitised payload safe for API responses.
* `SYSTEM` errors replace the message with a generic string; `DOMAIN` errors include `details`.
*/
toResponse() {
const resp = {
code: this.code,
message: this.message,
};
if (this.kind === ErrorKind.SYSTEM) {
resp.message = 'Houston, we have a problem';
}
else if (this.kind === ErrorKind.DOMAIN) {
resp.details = this.details;
}
return resp;
}
/**
* Parses an unknown value into `ElevaErrorArgs`. Shared by `parse()` and `RemoteElevaError.from()`.
*
* Strategy: validate against `errorSchema` → look up known codes in the catalog →
* fall back to `INTERNAL_SERVER_ERROR` for unrecognised codes or malformed payloads →
* accept legacy `errors` field as a fallback for `details`.
*/
static _parsePayload(value) {
const result = errorSchema.safeParse(value);
if (!result.success) {
const code = ERROR_CODES.INTERNAL_SERVER_ERROR;
const entry = ERRORS_CATALOG[code];
return {
code,
kind: entry.kind,
status: entry.status,
message: `Failed to parse error payload: ${result.error.message}`,
details: {},
cause: value,
};
}
const code = result.data.code;
const isKnownCode = KNOWN_CODES.has(code);
const isValidKind = !!result.data.kind && VALID_KINDS.has(result.data.kind);
const entry = isKnownCode ? ERRORS_CATALOG[code] : ERRORS_CATALOG[ERROR_CODES.INTERNAL_SERVER_ERROR];
return {
code,
kind: isValidKind ? result.data.kind : entry.kind,
status: result.data.status ?? entry.status,
message: result.data.message ?? entry.message,
details: (result.data.details ?? result.data.errors ?? {}),
};
}
/** Deserialises an unknown value into an `ElevaError`. Unrecognised payloads become `INTERNAL_SERVER_ERROR`. */
static parse(value) {
return new ElevaError(ElevaError._parsePayload(value));
}
/** Converts a `ZodError` into a `VALIDATION` error, preserving field paths and rule codes. */
static fromZodError(error) {
const errors = error.issues.map((issue) => {
const path = issue.path.map(String);
const validation = path.join('.') || 'root';
return { validation, code: issue.code, message: issue.message, path };
});
return ElevaError.validation({ errors, message: error.message });
}
/** Creates a `VALIDATION` error from a pre-built array of issue descriptors. */
static validation(args) {
return _newElevaErrorFromCode(BASE_ERROR_CODES.VALIDATION, {
details: { errors: args.errors },
message: args.message,
});
}
/** Creates a `VALIDATION` error for a single invalid field. Convenience wrapper around `validation()`. */
static invalidField(args) {
return ElevaError.validation({ errors: [args.error], message: args.message });
}
/** HTTP 400 — request is malformed or has invalid parameters outside of validation. */
static badRequest = _optionalFactory(BASE_ERROR_CODES.BAD_REQUEST);
/** HTTP 401 — credentials are missing, expired, or invalid. Caller must re-authenticate. */
static authentication = _optionalFactory(BASE_ERROR_CODES.AUTHENTICATION);
/** HTTP 403 — authenticated but lacks the required scope or permission. */
static unauthorized = _optionalFactory(BASE_ERROR_CODES.UNAUTHORIZED);
/**
* HTTP 404 — no resource found for the given identifier.
* @param args.entity - Entity type that was not found (e.g. `"Invoice"`).
*/
static notFound = _namedFieldFactory(BASE_ERROR_CODES.NOT_FOUND, 'entity');
/** HTTP 406 — server cannot produce a response matching the `Accept` header. */
static notAcceptable = _optionalFactory(BASE_ERROR_CODES.NOT_ACCEPTABLE);
/** HTTP 409 — operation violates a uniqueness or integrity constraint. */
static conflict = _requiredDetailsFactory(BASE_ERROR_CODES.CONFLICT);
/** HTTP 410 — resource permanently removed; retrying will never succeed. */
static gone = _optionalFactory(BASE_ERROR_CODES.GONE);
/** HTTP 422 — request is syntactically valid but semantically unprocessable. */
static unprocessableEntity = _optionalFactory(BASE_ERROR_CODES.UNPROCESSABLE_ENTITY);
/**
* HTTP 502 — upstream service returned an invalid response (`SYSTEM`).
* @param args.details.upstream - Name of the misbehaving upstream service.
*/
static badGateway = _optionalFactory(BASE_ERROR_CODES.BAD_GATEWAY);
/** HTTP 503 — service temporarily unavailable (`SYSTEM`). Pass `retryAfter` (seconds) if known. */
static serviceUnavailable = _optionalFactory(BASE_ERROR_CODES.SERVICE_UNAVAILABLE);
/**
* HTTP 400 — a required field was missing from the request.
* @param args.field - Name of the missing field.
*/
static mandatoryField = _namedFieldFactory(BASE_ERROR_CODES.MANDATORY_FIELD, 'field');
/**
* HTTP 500 — unexpected server error (`SYSTEM`).
* Pass the original exception as `cause` to preserve the full stack chain for logging.
*/
static internal(args = {}) {
return _newElevaErrorFromCode(BASE_ERROR_CODES.INTERNAL_SERVER_ERROR, {
details: args.details ?? {},
message: args.message,
cause: args.cause,
});
}
static forbiddenContact(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.FORBIDDEN_CONTACT, {
details: args.details,
message: args.message,
});
}
/**
* HTTP 413 — bulk messaging attachments exceed the maximum allowed total size.
* @param args.details.maxBytes - Maximum total size allowed, in bytes.
* @param args.details.totalBytes - Total size of the submitted attachments, in bytes.
*/
static horizontalBulkMessagingAttachmentsTooLarge = _requiredDetailsFactory(API_ERROR_CODES.HORIZONTAL_BULK_MESSAGING_ATTACHMENTS_TOO_LARGE);
static horizontalClaimPolicyCommunityMismatch(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.HORIZONTAL_CLAIM_POLICY_COMMUNITY_MISMATCH, {
details: args.details,
message: args.message,
});
}
static horizontalClaimContactAlreadyLinked(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.HORIZONTAL_CLAIM_CONTACT_ALREADY_LINKED, {
details: args.details,
message: args.message,
});
}
static horizontalClaimClosedBeforeOpened(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.HORIZONTAL_CLAIM_CLOSED_BEFORE_OPENED, {
details: args.details,
message: args.message,
});
}
static ticketingTaskClaimSubRefNotAllowed(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.TICKETING_TASK_CLAIM_SUBREF_NOT_ALLOWED, {
details: args.details,
message: args.message,
});
}
static ticketingTaskClaimAlreadyAssociated(args) {
return _newElevaErrorFromCode(API_ERROR_CODES.TICKETING_TASK_CLAIM_ALREADY_ASSOCIATED, {
details: args.details,
message: args.message,
});
}
}
//# sourceMappingURL=errors.js.map