@eleva-io/erp-sdk
Version:
SDK oficial para el ERP de Eleva
284 lines • 12.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElevaError = void 0;
const zod_1 = require("zod");
const catalog_1 = require("./catalog");
const catalog_types_1 = require("./catalog_types");
const catalog_base_1 = require("./catalog_base");
const catalog_api_1 = require("./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 = zod_1.z.object({
code: zod_1.z.string(),
kind: zod_1.z.string().optional(),
status: zod_1.z.number().optional(),
message: zod_1.z.string().optional(),
details: zod_1.z.unknown().optional(),
/** @deprecated Use `details` instead */
errors: zod_1.z.unknown().optional(),
});
/** Pre-computed set of all valid error code strings for O(1) membership checks. */
const KNOWN_CODES = new Set(Object.keys(catalog_1.ERROR_CODES));
/** Pre-computed set of all valid `ErrorKind` values for O(1) membership checks. */
const VALID_KINDS = new Set(Object.values(catalog_types_1.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 = catalog_1.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.
*/
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 === catalog_types_1.ErrorKind.SYSTEM) {
resp.message = 'Houston, we have a problem';
}
else if (this.kind === catalog_types_1.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 = catalog_1.ERROR_CODES.INTERNAL_SERVER_ERROR;
const entry = catalog_1.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 ? catalog_1.ERRORS_CATALOG[code] : catalog_1.ERRORS_CATALOG[catalog_1.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(catalog_base_1.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(catalog_base_1.BASE_ERROR_CODES.BAD_REQUEST);
/** HTTP 401 — credentials are missing, expired, or invalid. Caller must re-authenticate. */
static authentication = _optionalFactory(catalog_base_1.BASE_ERROR_CODES.AUTHENTICATION);
/** HTTP 403 — authenticated but lacks the required scope or permission. */
static unauthorized = _optionalFactory(catalog_base_1.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(catalog_base_1.BASE_ERROR_CODES.NOT_FOUND, 'entity');
/** HTTP 406 — server cannot produce a response matching the `Accept` header. */
static notAcceptable = _optionalFactory(catalog_base_1.BASE_ERROR_CODES.NOT_ACCEPTABLE);
/** HTTP 409 — operation violates a uniqueness or integrity constraint. */
static conflict = _requiredDetailsFactory(catalog_base_1.BASE_ERROR_CODES.CONFLICT);
/** HTTP 410 — resource permanently removed; retrying will never succeed. */
static gone = _optionalFactory(catalog_base_1.BASE_ERROR_CODES.GONE);
/** HTTP 422 — request is syntactically valid but semantically unprocessable. */
static unprocessableEntity = _optionalFactory(catalog_base_1.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(catalog_base_1.BASE_ERROR_CODES.BAD_GATEWAY);
/** HTTP 503 — service temporarily unavailable (`SYSTEM`). Pass `retryAfter` (seconds) if known. */
static serviceUnavailable = _optionalFactory(catalog_base_1.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(catalog_base_1.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(catalog_base_1.BASE_ERROR_CODES.INTERNAL_SERVER_ERROR, {
details: args.details ?? {},
message: args.message,
cause: args.cause,
});
}
static forbiddenContact(args) {
return _newElevaErrorFromCode(catalog_api_1.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(catalog_api_1.API_ERROR_CODES.HORIZONTAL_BULK_MESSAGING_ATTACHMENTS_TOO_LARGE);
static horizontalClaimPolicyCommunityMismatch(args) {
return _newElevaErrorFromCode(catalog_api_1.API_ERROR_CODES.HORIZONTAL_CLAIM_POLICY_COMMUNITY_MISMATCH, {
details: args.details,
message: args.message,
});
}
static horizontalClaimContactAlreadyLinked(args) {
return _newElevaErrorFromCode(catalog_api_1.API_ERROR_CODES.HORIZONTAL_CLAIM_CONTACT_ALREADY_LINKED, {
details: args.details,
message: args.message,
});
}
static horizontalClaimClosedBeforeOpened(args) {
return _newElevaErrorFromCode(catalog_api_1.API_ERROR_CODES.HORIZONTAL_CLAIM_CLOSED_BEFORE_OPENED, {
details: args.details,
message: args.message,
});
}
static ticketingTaskClaimSubRefNotAllowed(args) {
return _newElevaErrorFromCode(catalog_api_1.API_ERROR_CODES.TICKETING_TASK_CLAIM_SUBREF_NOT_ALLOWED, {
details: args.details,
message: args.message,
});
}
static ticketingTaskClaimAlreadyAssociated(args) {
return _newElevaErrorFromCode(catalog_api_1.API_ERROR_CODES.TICKETING_TASK_CLAIM_ALREADY_ASSOCIATED, {
details: args.details,
message: args.message,
});
}
}
exports.ElevaError = ElevaError;
//# sourceMappingURL=errors.js.map