UNPKG

zagora

Version:

A minimalist & robust way to create type-safe and error-safe never throwing functions & libraries in TypeScript - with input/output validation and typed errors. Based on StandardSchema-compliant validation libraries. No batteries, no routers, it's just fu

309 lines (307 loc) 11.5 kB
//#region src/utils.ts var ZagoraError = class ZagoraError extends Error { type = "ZAGORA_ERROR"; issues; cause; reason; constructor(message, options) { super(message); this.name = "ZagoraError"; this.issues = options?.issues; this.cause = options?.cause; this.reason = options?.reason || "Unknown or internal error"; } static fromIssues(issues) { const message = issues.map((issue) => issue.message).join(", "); return new ZagoraError(message, { issues, reason: "Failure caused by validation" }); } static fromCaughtError(caught, reason) { const message = caught instanceof Error ? caught.message : String(caught); return new ZagoraError(reason || message, { cause: caught, reason }); } }; function createDualResult(data, error, isDefined) { const result = [ data, error, isDefined ]; result.data = data; result.error = error; result.isDefined = isDefined; return result; } //#endregion //#region src/index.ts function zagora(config) { return new Zagora(config); } var Zagora = class Zagora { _inputSchema = null; _outputSchema = null; _errorSchema = null; _config; "~zagora"; constructor(config) { this._errorSchema = null; this._config = config || void 0; } input(schema) { const next = new Zagora(this._config); next._inputSchema = schema; next._outputSchema = this._outputSchema; next._errorSchema = this._errorSchema; this["~zagora"] = { inputSchema: schema, outputSchema: this._outputSchema, errorSchema: this._errorSchema, handlerFn: null }; return next; } output(schema) { const next = new Zagora(this._config); next._inputSchema = this._inputSchema; next._outputSchema = schema; next._errorSchema = this._errorSchema; this["~zagora"] = { inputSchema: this._inputSchema, outputSchema: schema, errorSchema: this._errorSchema, handlerFn: null }; return next; } errors(schema) { const next = new Zagora(this._config); next._inputSchema = this._inputSchema; next._outputSchema = this._outputSchema; next._errorSchema = schema; this["~zagora"] = { inputSchema: this._inputSchema, outputSchema: this._outputSchema, errorSchema: schema, handlerFn: null }; return next; } handler(impl) { const handlerFn = this.createHandlerAsync(impl); this["~zagora"] = { inputSchema: this._inputSchema, outputSchema: this._outputSchema, errorSchema: this._errorSchema, handlerFn }; return Object.assign(handlerFn, this); } handlerSync(impl) { const handlerFn = this.createHandlerSync(impl); this["~zagora"] = { inputSchema: this._inputSchema, outputSchema: this._outputSchema, errorSchema: this._errorSchema, handlerFn }; return Object.assign(handlerFn, this); } createHandlerSync(impl) { if (!this._inputSchema) throw new Error(".input(...) must be called first"); if (!this._outputSchema) throw new Error(".output(...) must be called first"); const inputSchema = this._inputSchema; const outputSchema = this._outputSchema; const errSchema = this._errorSchema; const wrapper = (...rawArgs) => { const inputResult = this.validateInputSync(inputSchema, rawArgs); if (inputResult.error) return createDualResult(null, inputResult.error, false); try { const finalArgs = errSchema ? this._config?.errorsFirst ? [this.createErrorHelpers(errSchema), ...inputResult.args] : [...inputResult.args, this.createErrorHelpers(errSchema)] : inputResult.args; const rawResult = impl(...finalArgs); if (rawResult instanceof Promise) return createDualResult(null, new ZagoraError("Using `.handlerSync` only accepts synchronous functions"), false); if (Array.isArray(rawResult) && rawResult.length === 2) { const [maybeOut, maybeErr] = rawResult; if (maybeErr != null) { if (errSchema) { const { error: validatedError, isTyped } = this.validateError(errSchema, maybeErr, true); if (isTyped) return createDualResult(null, validatedError, true); return createDualResult(null, validatedError, true); } const zagoraError = maybeErr instanceof ZagoraError ? maybeErr : ZagoraError.fromCaughtError(maybeErr, "Untyped error returned"); return createDualResult(null, zagoraError, false); } const [res, err] = this.validateOutput(outputSchema, maybeOut, true); if (err === null) return createDualResult(res, null, false); return createDualResult(null, err, false); } const [data, error] = this.validateOutput(outputSchema, rawResult, true); if (error === null) return createDualResult(data, null, false); return createDualResult(null, error, false); } catch (err) { const zagoraError = ZagoraError.fromCaughtError(err, "Handler threw unknown error"); return createDualResult(null, zagoraError, false); } }; const forwardImpl = (...args) => wrapper(...args); return forwardImpl; } createHandlerAsync(impl) { if (!this._inputSchema) throw new Error(".input(...) must be called first"); if (!this._outputSchema) throw new Error(".output(...) must be called first"); const inputSchema = this._inputSchema; const outputSchema = this._outputSchema; const errSchema = this._errorSchema; const wrapper = async (...rawArgs) => { const inputResult = await this.validateInput(inputSchema, rawArgs); if (inputResult.error) return createDualResult(null, inputResult.error, false); try { const finalArgs = errSchema ? this._config?.errorsFirst ? [this.createErrorHelpers(errSchema), ...inputResult.args] : [...inputResult.args, this.createErrorHelpers(errSchema)] : inputResult.args; let rawResult = impl(...finalArgs); if (!(rawResult instanceof Promise)) return createDualResult(null, new ZagoraError("Using `.handler` only accepts async functions"), false); rawResult = await rawResult; if (Array.isArray(rawResult) && rawResult.length === 2) { const [maybeOut, maybeErr] = rawResult; if (maybeErr != null) { if (errSchema) { const { error: validatedError, isTyped } = await this.validateError(errSchema, maybeErr, false); if (isTyped) return createDualResult(null, validatedError, true); return createDualResult(null, validatedError, false); } const zagoraError = maybeErr instanceof ZagoraError ? maybeErr : ZagoraError.fromCaughtError(maybeErr, "Untyped error returned"); return createDualResult(null, zagoraError, false); } const [res, err] = await this.validateOutput(outputSchema, maybeOut, false); if (err === null) return createDualResult(res, null, false); return createDualResult(null, err, false); } const [data, error] = await this.validateOutput(outputSchema, rawResult, false); if (error === null) return createDualResult(data, null, false); return createDualResult(null, error, false); } catch (err) { const zagoraError = ZagoraError.fromCaughtError(err, "Handler threw unknown error"); return createDualResult(null, zagoraError, false); } }; const forwardImpl = (...args) => wrapper(...args); return forwardImpl; } validateInputSync(inputSchema, rawArgs) { const processedArgs = this.handleTupleDefaults(inputSchema, rawArgs); let result = inputSchema["~standard"].validate(processedArgs); if (result instanceof Promise) throw new ZagoraError("Cannot use async input schema validation in handlerSync"); if (!result.issues) return { args: result.value }; const singleValue = processedArgs[0]; result = inputSchema["~standard"].validate(singleValue); if (result instanceof Promise) throw new ZagoraError("Cannot use async input schema validation in handlerSync"); if (result.issues) return { args: [], error: ZagoraError.fromIssues(result.issues) }; const validatedValue = result.value; return { args: Array.isArray(validatedValue) ? validatedValue : [validatedValue] }; } async validateInput(inputSchema, rawArgs) { const processedArgs = this.handleTupleDefaults(inputSchema, rawArgs); let result = inputSchema["~standard"].validate(processedArgs); if (result instanceof Promise) result = await result; if (!result.issues) return { args: result.value }; const singleValue = processedArgs[0]; result = inputSchema["~standard"].validate(singleValue); if (result instanceof Promise) result = await result; if (result.issues) return { args: [], error: ZagoraError.fromIssues(result.issues) }; const validatedValue = result.value; return { args: Array.isArray(validatedValue) ? validatedValue : [validatedValue] }; } handleTupleDefaults(schema, rawArgs) { const schemaAny = schema; if (schemaAny._def && schemaAny._def.type === "tuple") { const tupleItems = schemaAny._def.items; if (tupleItems && Array.isArray(tupleItems)) { const result = [...rawArgs]; for (let i = rawArgs.length; i < tupleItems.length; i++) { const itemSchema = tupleItems[i]; if (itemSchema && itemSchema.type === "default" && itemSchema._def) result[i] = typeof itemSchema._def.defaultValue === "function" ? itemSchema._def.defaultValue() : itemSchema._def.defaultValue; } return result; } } return rawArgs; } validateOutput(outputSchema, output, isSync) { const result = outputSchema["~standard"].validate(output); if (result instanceof Promise) { if (isSync) throw new ZagoraError("Cannot use async output schema validation in handlerSync"); return result.then((res) => { if (res.issues) return [null, ZagoraError.fromIssues(res.issues)]; return [res.value, null]; }); } if (result.issues) return [null, ZagoraError.fromIssues(result.issues)]; return [result.value, null]; } validateError(errSchema, maybeErr, isSync) { for (const [_key, errorSchema] of Object.entries(errSchema)) { const result = errorSchema["~standard"].validate(maybeErr); if (result instanceof Promise) { if (isSync) throw new ZagoraError("Cannot use async error schema validation in handlerSync"); return result.then((res) => { if (!res.issues) return { error: res.value, isTyped: true }; return { error: maybeErr, isTyped: false }; }); } if (!result.issues) return { error: result.value, isTyped: true }; } return { error: maybeErr, isTyped: false }; } createErrorHelpers(schema) { const toPascalCase = (str) => { return str.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(""); }; const helpers = {}; for (const [key, errorSchema] of Object.entries(schema)) helpers[key] = (error) => { const typeVariants = [ key, `${toPascalCase(key)}Error`, `${key.toUpperCase()}_ERROR` ]; let result; let errorWithType; result = errorSchema["~standard"].validate(error); if (result instanceof Promise) throw new ZagoraError("Synchronous error helpers don't support async schemas"); if (!result.issues) return [null, result.value]; for (const typeValue of typeVariants) { errorWithType = { ...error, type: typeValue }; result = errorSchema["~standard"].validate(errorWithType); if (result instanceof Promise) throw new ZagoraError("Synchronous error helpers don't support async schemas"); if (!result.issues) return [null, result.value]; } throw new ZagoraError(`Invalid error data for "errors.${key}": ${result.issues.map((i) => i.message).join(", ")}`); }; return helpers; } }; //#endregion export { Zagora, ZagoraError, createDualResult, zagora };