UNPKG

@eleva-io/erp-sdk

Version:

SDK oficial para el ERP de Eleva

280 lines 11.9 kB
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