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
JavaScript
//#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 };