@palmares/schemas
Version:
This defines a default schema definition for validation of data, it abstract popular schema validation libraries like zod, yup, valibot and others"
1,226 lines (1,216 loc) • 231 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ArrayFieldAdapter: () => ArrayFieldAdapter,
ArraySchema: () => ArraySchema,
BooleanFieldAdapter: () => BooleanFieldAdapter,
BooleanSchema: () => BooleanSchema,
DatetimeFieldAdapter: () => DatetimeFieldAdapter,
DatetimeSchema: () => DatetimeSchema,
FieldAdapter: () => FieldAdapter,
NumberFieldAdapter: () => NumberFieldAdapter,
NumberSchema: () => NumberSchema,
ObjectFieldAdapter: () => ObjectFieldAdapter,
ObjectSchema: () => ObjectSchema,
Schema: () => Schema,
SchemaAdapter: () => SchemaAdapter,
SchemaDomain: () => schemasDomain,
StringFieldAdapter: () => StringFieldAdapter,
StringSchema: () => StringSchema,
UnionFieldAdapter: () => UnionFieldAdapter,
UnionSchema: () => UnionSchema,
array: () => array,
arrayFieldAdapter: () => arrayFieldAdapter,
boolean: () => boolean,
booleanFieldAdapter: () => booleanFieldAdapter,
compile: () => compile,
datetime: () => datetime,
datetimeFieldAdapter: () => datetimeFieldAdapter,
default: () => index_default,
fieldAdapter: () => fieldAdapter,
getDefaultAdapter: () => getDefaultAdapter,
getSchemasWithDefaultAdapter: () => getSchemasWithDefaultAdapter,
modelSchema: () => modelSchema,
number: () => number,
numberFieldAdapter: () => numberFieldAdapter,
object: () => object,
objectFieldAdapter: () => objectFieldAdapter,
schema: () => schema,
schemaHandler: () => schemaHandler,
setDefaultAdapter: () => setDefaultAdapter,
string: () => string,
stringFieldAdapter: () => stringFieldAdapter,
union: () => union,
unionFieldAdapter: () => unionFieldAdapter
});
module.exports = __toCommonJS(index_exports);
// src/exceptions.ts
var SchemaAdapterNotImplementedError = class extends Error {
static {
__name(this, "SchemaAdapterNotImplementedError");
}
constructor(args) {
super(`Schema adapter did not implement ${args.functionName} in ${args.className}`);
}
};
var NoAdapterFoundError = class extends Error {
static {
__name(this, "NoAdapterFoundError");
}
constructor() {
super("No adapter found, please define an adapter using setDefaultAdapter() before using any schema.");
}
};
// src/adapter/index.ts
var SchemaAdapter = class {
static {
__name(this, "SchemaAdapter");
}
$$type = "$PSchemaAdapter";
name;
field;
number;
object;
union;
string;
array;
boolean;
datetime;
adapterInstance;
// eslint-disable-next-line ts/require-await
async formatError(_error, _metadata) {
throw new SchemaAdapterNotImplementedError({
className: "SchemaAdapter",
functionName: "formatError"
});
}
};
// src/domain.ts
var import_core = require("@palmares/core");
// src/conf.ts
function setDefaultAdapter(adapter) {
globalThis.$PSchemasAdapter = adapter;
}
__name(setDefaultAdapter, "setDefaultAdapter");
function getDefaultAdapter() {
if (!globalThis.$PSchemasAdapter) throw new NoAdapterFoundError();
return globalThis.$PSchemasAdapter;
}
__name(getDefaultAdapter, "getDefaultAdapter");
// src/domain.ts
var schemasDomain = (0, import_core.domain)("@palmares/schemas", "", {
commands: {},
// eslint-disable-next-line ts/require-await
load: /* @__PURE__ */ __name(async (settings) => {
setDefaultAdapter(new settings.schemaAdapter());
const schemaAdapter = getDefaultAdapter();
return void 0;
}, "load")
});
// src/validators/schema.ts
function optional(args) {
return {
name: "optional",
type: "high",
// eslint-disable-next-line ts/require-await
callback: /* @__PURE__ */ __name(async (value, path) => {
if (value === void 0) {
if (args.allow === true) return {
parsed: value,
errors: [],
preventChildValidation: true
};
return {
parsed: value,
errors: [
{
isValid: false,
message: args.message,
code: "required",
// eslint-disable-next-line ts/no-unnecessary-condition
path: path || []
}
],
preventChildValidation: true
};
}
return {
parsed: value,
errors: [],
preventChildValidation: false
};
}, "callback")
};
}
__name(optional, "optional");
function nullable(args) {
return {
name: "nullable",
type: "high",
// eslint-disable-next-line ts/require-await
callback: /* @__PURE__ */ __name(async (value, path) => {
if (value === null) {
if (args.allow === true) return {
parsed: value,
errors: [],
preventChildValidation: true
};
return {
parsed: value,
errors: [
{
isValid: false,
message: args.message,
code: "null",
// eslint-disable-next-line ts/no-unnecessary-condition
path: path || []
}
],
preventChildValidation: true
};
}
return {
parsed: value,
errors: [],
preventChildValidation: false
};
}, "callback")
};
}
__name(nullable, "nullable");
function checkType(args) {
return {
name: "checkType",
type: "medium",
// eslint-disable-next-line ts/require-await
callback: /* @__PURE__ */ __name(async (value, path) => {
if (args.check(value)) return {
parsed: value,
errors: [],
preventChildValidation: false
};
return {
parsed: value,
errors: [
{
isValid: false,
message: args.message,
code: "invalid_type",
// eslint-disable-next-line ts/no-unnecessary-condition
path: path || []
}
],
preventChildValidation: true
};
}, "callback")
};
}
__name(checkType, "checkType");
function is(args) {
return {
name: "is",
type: "medium",
// eslint-disable-next-line ts/require-await
callback: /* @__PURE__ */ __name(async (value, path, _options) => {
const isValid = Array.isArray(args.value) ? args.value.includes(value) : value === args.value;
return {
parsed: value,
errors: isValid ? [] : [
{
isValid: false,
code: "is",
// eslint-disable-next-line ts/no-unnecessary-condition
path: path || [],
message: "Value is not a boolean"
}
],
preventChildValidation: true
};
}, "callback")
};
}
__name(is, "is");
// src/validators/utils.ts
var priorityByType = {
low: 0,
medium: 1,
high: 2
};
var typeByPriority = Object.entries(priorityByType).reduce((acc, [key, value]) => {
acc[value] = key;
return acc;
}, {});
var Validator = class _Validator {
static {
__name(this, "Validator");
}
$$type = "$PValidator";
child;
parent;
fallbackNamesAdded = /* @__PURE__ */ new Set();
priority;
fallbacks = [];
constructor(type) {
this.fallbackNamesAdded = /* @__PURE__ */ new Set();
this.fallbacks = [];
this.priority = priorityByType[type];
}
/**
* We create all of the validators on the schema in order, i actually didn't want to go on that route but i
* found it easier to do so.
*
* The logic here is simple, if it's not the same priority we will walk on the linked list until we find
* a validator that matches the priority we are expecting. If we can't walk anymore, we create the next
* priority validator and append it to the linked list. Be aware that it's a double linked list, so we
* can walk both ways, from the end to the start and from the start to the end.
* So you don't really need to start from the root, the linked list can start from anywhere and it will
* find it's way through.
*
* I know there are better ways to do this instead of walking through the linked list, but like i explained
* before, this is enough for now.
*
* If the priority is higher than the current priority saved on the schema, we should substitute the
* rootValidator on the schema with the new one.
*
* @param schema - The schema that we are working on right now, all fallbacks are tied to that specific schema.
* @param type - The type of the fallback that we are adding.
* @param fallback - The fallback function that we are adding.
* @param childOrParent - If we are adding a fallback to the child or to the parent.
* @param options - The options that we are passing to the fallback.
*/
checkAppendOrCreate(schema2, type, fallbackName, fallback, childOrParent, options) {
const schemaWithProtected = schema2;
if (this[childOrParent]) this[childOrParent].addFallback(schemaWithProtected, type, fallbackName, fallback, options);
else {
const nextPriority = childOrParent === "child" ? this.priority - 1 : this.priority + 1;
if (Object.keys(typeByPriority).includes(String(nextPriority))) {
const nextType = typeByPriority[nextPriority];
const validatorInstance = new _Validator(nextType);
this[childOrParent] = validatorInstance;
this[childOrParent][childOrParent === "parent" ? "child" : "parent"] = this;
this[childOrParent].addFallback(schemaWithProtected, type, fallbackName, fallback, options);
if (nextPriority > schemaWithProtected.__rootFallbacksValidator.priority) schemaWithProtected.__rootFallbacksValidator = validatorInstance;
}
}
}
addFallback(schema2, type, fallbackName, fallback, options) {
if (this.fallbackNamesAdded.has(fallbackName) && options?.removeCurrent !== true) return;
this.fallbackNamesAdded.add(fallbackName);
const priority = priorityByType[type];
if (this.priority === priority) {
if (typeof options?.at === "number") this.fallbacks.splice(options.at, options.removeCurrent === true ? 1 : 0, fallback);
else this.fallbacks.push(fallback);
} else if (priority > this.priority) this.checkAppendOrCreate(schema2, type, fallbackName, fallback, "parent", options);
else if (priority < this.priority) this.checkAppendOrCreate(schema2, type, fallbackName, fallback, "child", options);
}
/**
* Validates the value against all of the fallbacks, the fallbacks are executed in order, from the highest
* priority to the lowest priority. A validator can stop the execution of the other validators if it feels
* like so. Like on the example of a value being null or undefined.
*
* @param errorsAsHashedSet - This is a set that contains all of the errors that we already found, this is
* used to avoid duplicated errors.
* @param path - The path that we are validating right now.
* @param parseResult - The result of the parsing, it contains the parsed value and the errors that we found.
* @param options - The options that we are passing to the fallback.
*/
async validate(errorsAsHashedSet, path, parseResult, options) {
let doesItShouldPreventChildValidation = false;
for (const fallback of this.fallbacks) {
const { parsed, errors, preventChildValidation } = await fallback(parseResult.parsed, path, options);
parseResult.parsed = parsed;
for (const error of errors) {
if (error.isValid === false) {
const sortedError = Object.fromEntries(Object.entries(error).sort(([a], [b]) => a.localeCompare(b)));
const hashedError = JSON.stringify(sortedError);
if (errorsAsHashedSet.has(hashedError)) continue;
errorsAsHashedSet.add(hashedError);
if (!Array.isArray(parseResult.errors)) parseResult.errors = [];
parseResult.errors.push({
...error,
received: parseResult.parsed
});
}
}
doesItShouldPreventChildValidation = doesItShouldPreventChildValidation || preventChildValidation || false;
}
if (this.child && doesItShouldPreventChildValidation === false) return await this.child.validate(errorsAsHashedSet, path, parseResult, options);
return parseResult;
}
/**
* This static method takes care of everything for you. This means that you should only call this method
* for appending new fallbacks, it takes care of creating the root validator and making sure that the
* rootValidator on the schema is the highest priority one.
*
* @param schema - The schema that we are working on right now, all fallbacks are tied to that specific
* schema. We automatically define the rootValidator on the schema so you don't need to worry about that.
* @param fallback - The fallback that we are adding. This is an object that contains the type of the
* fallback and the callback that we are adding.
* @param options - The options that we are passing to the fallback. Options like `at` and `removeCurrent`
* are passed to the `addFallback` method.
*/
static createAndAppendFallback(schema2, fallback, options) {
const schemaWithProtected = schema2;
let validatorInstance = schemaWithProtected.__rootFallbacksValidator;
if (schemaWithProtected.__rootFallbacksValidator === void 0) {
validatorInstance = new _Validator(fallback.type);
schemaWithProtected.__rootFallbacksValidator = validatorInstance;
}
validatorInstance.addFallback(schema2, fallback.type, fallback.name, fallback.callback, options);
return validatorInstance;
}
toString(ident = 0) {
return `Priority: ${this.priority}
Fallbacks: ${this.fallbacks.length}
${this.child ? `Children:
${this.child.toString(ident + 2)}` : ""}`;
}
};
// src/utils.ts
var WithFallback = class {
static {
__name(this, "WithFallback");
}
$$type = "$PWithFallback";
fallbackFor;
transformedSchema;
adapterType;
constructor(adapterType, fallbackFor, transformedSchema) {
this.adapterType = adapterType;
this.fallbackFor = new Set(fallbackFor);
this.transformedSchema = transformedSchema;
}
};
function withFallbackFactory(adapterType) {
return (fallbackFor, transformedSchema) => new WithFallback(adapterType, fallbackFor, transformedSchema);
}
__name(withFallbackFactory, "withFallbackFactory");
async function defaultTransform(type, schema2, adapter, fieldAdapter2, getValidationData, fallbackFunctions, options) {
const validationData = await Promise.resolve(getValidationData(false));
const validationDataForStringVersion = options.shouldAddStringVersion ? await Promise.resolve(getValidationData(true)) : void 0;
const schemaWithPrivateFields = schema2;
const checkIfShouldUseParserAndAppend = /* @__PURE__ */ __name((parser) => {
const isValidationDataAParser = validationData.parsers?.[parser] !== void 0;
if (isValidationDataAParser) schema2.__parsers._fallbacks.add(parser);
}, "checkIfShouldUseParserAndAppend");
const getExtendedOrNotSchemaAndString = /* @__PURE__ */ __name((schema3, toStringVersion) => {
const extendedOrNotSchema2 = typeof schemaWithPrivateFields.__extends?.callback === "function" ? schemaWithPrivateFields.__extends.callback(schema3) : schema3;
const extendedOrNotSchemaString2 = typeof schemaWithPrivateFields.__extends?.toStringCallback === "function" ? schemaWithPrivateFields.__extends.toStringCallback(toStringVersion) : toStringVersion;
return [
extendedOrNotSchema2,
extendedOrNotSchemaString2
];
}, "getExtendedOrNotSchemaAndString");
const checkIfShouldAppendFallbackAndAppend = /* @__PURE__ */ __name((fallback) => {
const wereArgumentsForThatFallbackDefinedAndFallbackFunctionDefined = validationData[fallback] !== void 0 && fallbackFunctions[fallback] !== void 0;
if (wereArgumentsForThatFallbackDefinedAndFallbackFunctionDefined) {
const fallbackReturnType = fallbackFunctions[fallback](validationData[fallback]);
Validator.createAndAppendFallback(schema2, fallbackReturnType);
}
}, "checkIfShouldAppendFallbackAndAppend");
const appendRootFallback = /* @__PURE__ */ __name(() => {
if (options.validatorsIfFallbackOrNotSupported) {
const validatorsIfFallbackOrNotSupported = Array.isArray(options.validatorsIfFallbackOrNotSupported) ? options.validatorsIfFallbackOrNotSupported : [
options.validatorsIfFallbackOrNotSupported
];
for (const fallback of validatorsIfFallbackOrNotSupported) Validator.createAndAppendFallback(schema2, fallback);
}
}, "appendRootFallback");
const appendRequiredFallbacks = /* @__PURE__ */ __name(() => {
const hasFallbacks = schemaWithPrivateFields.__rootFallbacksValidator?.["$$type"] === "$PValidator";
if (hasFallbacks) {
Validator.createAndAppendFallback(schema2, optional(schemaWithPrivateFields.__optional));
Validator.createAndAppendFallback(schema2, nullable(schemaWithPrivateFields.__nullable));
Validator.createAndAppendFallback(schema2, checkType(schemaWithPrivateFields.__type));
}
}, "appendRequiredFallbacks");
const isFieldAdapterNotSupportedForThatFieldType = fieldAdapter2 === void 0;
if (options.fallbackIfNotSupported !== void 0 && isFieldAdapterNotSupportedForThatFieldType) {
const existingFallbacks = Object.keys(fallbackFunctions);
const allParsers = Object.keys(validationData["parsers"]);
appendRootFallback();
for (const fallback of existingFallbacks) checkIfShouldAppendFallbackAndAppend(fallback);
for (const parser of allParsers) checkIfShouldUseParserAndAppend(parser);
appendRequiredFallbacks();
return options.fallbackIfNotSupported();
}
if (!fieldAdapter2) throw new Error("The field adapter is not supported and no fallback was provided.");
const translatedSchemaOrWithFallback = await Promise.resolve(fieldAdapter2.translate(adapter.field, {
withFallback: withFallbackFactory(type),
...validationData
}));
let stringVersion = "";
if (options.shouldAddStringVersion) stringVersion = await fieldAdapter2.toString(adapter, adapter.field, validationDataForStringVersion);
if (translatedSchemaOrWithFallback?.["$$type"] === "$PWithFallback") {
appendRootFallback();
for (const fallback of translatedSchemaOrWithFallback.fallbackFor) {
checkIfShouldAppendFallbackAndAppend(fallback);
checkIfShouldUseParserAndAppend(fallback);
}
const [extendedOrNotSchema2, extendedOrNotSchemaString2] = getExtendedOrNotSchemaAndString(translatedSchemaOrWithFallback.transformedSchema, stringVersion);
appendRequiredFallbacks();
return [
{
transformed: extendedOrNotSchema2,
asString: extendedOrNotSchemaString2
}
];
}
const [extendedOrNotSchema, extendedOrNotSchemaString] = getExtendedOrNotSchemaAndString(translatedSchemaOrWithFallback, stringVersion);
return [
{
transformed: extendedOrNotSchema,
asString: extendedOrNotSchemaString
}
];
}
__name(defaultTransform, "defaultTransform");
async function defaultTransformToAdapter(callback, schema2, transformedSchemas, options, type) {
const isTransformedSchemasEmpty = Object.keys(transformedSchemas).length <= 0;
if (isTransformedSchemasEmpty) {
const adapterInstanceToUse = (
// eslint-disable-next-line ts/no-unnecessary-condition
options.schemaAdapter?.["$$type"] === "$PSchemaAdapter" ? options.schemaAdapter : getDefaultAdapter()
);
schema2["__transformedSchemas"][adapterInstanceToUse.name] = {
transformed: false,
adapter: adapterInstanceToUse,
schemas: []
};
}
const schemaAdapterNameToUse = options.schemaAdapter?.name || Object.keys(transformedSchemas)[0];
const isACustomSchemaAdapterAndNotYetDefined = (
// eslint-disable-next-line ts/no-unnecessary-condition
transformedSchemas[schemaAdapterNameToUse] === void 0 && options.schemaAdapter !== void 0
);
if (isACustomSchemaAdapterAndNotYetDefined) transformedSchemas[schemaAdapterNameToUse] = {
transformed: false,
adapter: options.schemaAdapter,
schemas: []
};
const shouldTranslate = transformedSchemas[schemaAdapterNameToUse].transformed === false || options.force === true;
if (shouldTranslate) {
const translatedSchemas = await callback(transformedSchemas[schemaAdapterNameToUse].adapter);
transformedSchemas[schemaAdapterNameToUse].schemas = translatedSchemas;
transformedSchemas[schemaAdapterNameToUse].transformed = true;
}
transformedSchemas[schemaAdapterNameToUse].transformed = true;
return transformedSchemas[schemaAdapterNameToUse].schemas;
}
__name(defaultTransformToAdapter, "defaultTransformToAdapter");
async function formatErrorFromParseMethod(adapter, fieldAdapter2, error, received, schema2, path, errorsAsHashedSet) {
const formattedError = await fieldAdapter2.formatError(adapter, adapter.field, schema2, error);
formattedError.path = Array.isArray(formattedError.path) ? [
...path,
...formattedError.path
] : path;
const formattedErrorAsParseResultError = formattedError;
formattedErrorAsParseResultError.isValid = false;
const sortedError = Object.fromEntries(Object.entries(formattedErrorAsParseResultError).sort(([a], [b]) => a.localeCompare(b)));
const hashedError = JSON.stringify(sortedError);
errorsAsHashedSet.add(JSON.stringify(sortedError));
formattedErrorAsParseResultError.received = received;
return formattedErrorAsParseResultError;
}
__name(formatErrorFromParseMethod, "formatErrorFromParseMethod");
async function transformSchemaAndCheckIfShouldBeHandledByFallbackOnComplexSchemas(schema2, options) {
const schemaWithProtected = schema2;
const transformedData = await schemaWithProtected.__transformToAdapter(options);
const doesKeyHaveFallback = schemaWithProtected.__rootFallbacksValidator !== void 0;
const doesKeyHaveToInternal = typeof schemaWithProtected.__toInternal === "function";
const doesKeyHaveToValidate = typeof schemaWithProtected.__toValidate === "function";
const doesKeyHaveToDefault = typeof schemaWithProtected.__defaultFunction === "function";
const doesKeyHaveRunBeforeParseAndData = typeof schemaWithProtected.__runBeforeParseAndData === "function";
const doesKeyHaveParserFallback = schemaWithProtected.__parsers._fallbacks.size > 0;
const shouldAddFallbackValidation = doesKeyHaveFallback || doesKeyHaveToInternal || doesKeyHaveToValidate || doesKeyHaveToDefault || doesKeyHaveParserFallback || doesKeyHaveRunBeforeParseAndData || // eslint-disable-next-line ts/no-unnecessary-condition
transformedData === void 0;
return [
transformedData,
shouldAddFallbackValidation
];
}
__name(transformSchemaAndCheckIfShouldBeHandledByFallbackOnComplexSchemas, "transformSchemaAndCheckIfShouldBeHandledByFallbackOnComplexSchemas");
function shouldRunDataOnComplexSchemas(schema2) {
return schema2["__parsers"].high.size > 0 || schema2["__parsers"].medium.size > 0 || schema2["__parsers"].low.size > 0 || schema2["__parsers"]._fallbacks.size > 0 || typeof schema2["__runBeforeParseAndData"] === "function" || typeof schema2["__toRepresentation"] === "function" || typeof schema2["__defaultFunction"] === "function";
}
__name(shouldRunDataOnComplexSchemas, "shouldRunDataOnComplexSchemas");
// src/schema/schema.ts
var Schema = class _Schema {
static {
__name(this, "Schema");
}
"~standard";
$$type = "$PSchema";
fieldType = "schema";
// Those functions will assume control of the validation process on adapters, instead of the schema.
// Why this is used? The idea is that the Schema has NO idea
// that one of it's children might be an UnionSchema for example. The adapter might not support unions,
// so then we give control to the union. The parent schema will already have an array of translated
// adapter schemas. This means for a union with Number and String it'll generate two schemas, one for number
// and one for the value as String. Of course this gets multiplied. So if we have a union with Number and String.
// We should take those two schemas from the array and validate them individually. This logic is
// handled by the union schema. If we have an intersection type for example, instead of validating
// One schema OR the other, we validate one schema AND the other. This will be handled
// by the schema that contains that intersection logic.
__beforeValidationCallbacks = /* @__PURE__ */ new Map();
__cachedGetParent;
set __getParent(value) {
this.__cachedGetParent = value;
}
get __getParent() {
return this.__cachedGetParent;
}
__alreadyAppliedModel;
__runBeforeParseAndData;
__rootFallbacksValidator;
__saveCallback;
__modelOmitCallback;
__parsers = {
high: /* @__PURE__ */ new Map(),
medium: /* @__PURE__ */ new Map(),
low: /* @__PURE__ */ new Map(),
_fallbacks: /* @__PURE__ */ new Set()
};
__refinements = [];
__nullable = {
message: "Cannot be null",
allow: false
};
__optional = {
message: "Required",
allow: false
};
__extends = void 0;
__transformedSchemas = {};
__defaultFunction = void 0;
__toRepresentation = void 0;
__toValidate = void 0;
__toInternal = void 0;
__type = {
message: "Invalid type",
check: /* @__PURE__ */ __name(() => true, "check")
};
__getDefaultTransformedSchemas() {
const adapterInstance = getDefaultAdapter();
if (this.__transformedSchemas[adapterInstance.constructor.name] === void 0) this.__transformedSchemas[adapterInstance.constructor.name] = {
transformed: false,
adapter: adapterInstance,
schemas: []
};
}
/**
* This will validate the data with the fallbacks, so internally, without relaying on the schema adapter.
* This is nice because we can support things that the schema adapter is not able to support by default.
*
* @param errorsAsHashedSet - The errors as a hashed set. This is used to prevent duplicate errors.
* @param path - The path of the error.
* @param parseResult - The result of the parse method.
*/
async __validateByFallbacks(path, parseResult, options) {
if (this.__rootFallbacksValidator) return this.__rootFallbacksValidator.validate(options.errorsAsHashedSet, path, parseResult, options);
return parseResult;
}
/**
* This will validate by the adapter. In other words, we send the data to the schema adapter and then we validate
* that data.
* So understand that, first we send the data to the adapter, the adapter validates it, then, after we validate
* from the adapter we validate with the fallbacks so we can do all of the extra validations not handled by
* the adapter.
*
* @param value - The value to be validated.
* @param errorsAsHashedSet - The errors as a hashed set. This is used to prevent duplicate errors on the validator.
* @param path - The path of the error so we can construct an object with the nested paths of the error.
* @param parseResult - The result of the parse method.
*
* @returns The result and the errors of the parse method.
*/
async __validateByAdapter(adapter, fieldAdapter2, schema2, value, path, options) {
const parseResult = {
errors: [],
parsed: value
};
parseResult.errors = [];
parseResult.parsed = value;
if (fieldAdapter2 === void 0 || typeof fieldAdapter2.parse !== "function") return parseResult;
const adapterParseResult = await fieldAdapter2.parse(adapter, adapter.field, schema2.transformed, value, options.args);
parseResult.parsed = adapterParseResult.parsed;
if (adapterParseResult.errors) {
if (Array.isArray(adapterParseResult.errors)) parseResult.errors = await Promise.all(adapterParseResult.errors.map(async (error) => formatErrorFromParseMethod(adapter, fieldAdapter2, error, value, schema2.transformed, path, options.errorsAsHashedSet || /* @__PURE__ */ new Set())));
else parseResult.errors = [
await formatErrorFromParseMethod(adapter, fieldAdapter2, parseResult.errors, value, schema2.transformed, path, options.errorsAsHashedSet || /* @__PURE__ */ new Set())
];
}
parseResult.errors = parseResult.errors.filter((error) => typeof error !== "undefined");
return parseResult;
}
// eslint-disable-next-line ts/require-await
async __transformToAdapter(_options) {
throw new Error("Not implemented");
}
/** */
async __parsersToTransformValue(value, parsersToUse) {
let shouldStop = false;
for (const [parserName, parser] of this.__parsers.high.entries()) {
if (parsersToUse instanceof Set === false || parsersToUse.has(parserName)) {
const result = await Promise.resolve(parser(value));
if (result.preventNextParsers) shouldStop = true;
value = result.value;
} else continue;
}
if (shouldStop === false) {
for (const [parserName, parser] of this.__parsers.medium.entries()) {
if (parsersToUse instanceof Set === false || parsersToUse.has(parserName)) {
const result = await Promise.resolve(parser(value));
if (result.preventNextParsers) shouldStop = true;
value = result.value;
} else continue;
}
}
if (shouldStop === false) {
for (const [parserName, parser] of this.__parsers.low.entries()) {
if (parsersToUse instanceof Set === false || parsersToUse.has(parserName)) {
const result = await Promise.resolve(parser(value));
if (result.preventNextParsers) shouldStop = true;
value = result.value;
} else continue;
}
}
return value;
}
async __parse(value, path = [], options) {
this.__getDefaultTransformedSchemas();
if (typeof this.__runBeforeParseAndData === "function") await this.__runBeforeParseAndData(this);
const shouldRunToInternalToBubbleUp = options.toInternalToBubbleUp === void 0;
if (shouldRunToInternalToBubbleUp) options.toInternalToBubbleUp = [];
if (options.errorsAsHashedSet instanceof Set === false) options.errorsAsHashedSet = /* @__PURE__ */ new Set();
const shouldCallDefaultFunction = value === void 0 && typeof this.__defaultFunction === "function";
const shouldCallToValidateCallback = typeof this.__toValidate === "function";
const schemaAdapterFieldType = this.fieldType;
if (shouldCallDefaultFunction) value = await this.__defaultFunction();
if (shouldCallToValidateCallback) value = await Promise.resolve(this.__toValidate(value));
const parseResult = {
errors: [],
parsed: value
};
value = await this.__parsersToTransformValue(value, this.__parsers._fallbacks);
if (options.appendFallbacksBeforeAdapterValidation === void 0) options.appendFallbacksBeforeAdapterValidation = (schema2, name, callback) => {
if (this !== schema2) this.__beforeValidationCallbacks.set(name, callback);
};
if (this.__transformedSchemas[options.schemaAdapter?.constructor.name || getDefaultAdapter().constructor.name].transformed === false) await this.__transformToAdapter(options);
const adapterToUse = options.schemaAdapter ? options.schemaAdapter : Object.values(this.__transformedSchemas)[0].adapter;
const parsedResultsAfterFallbacks = await this.__validateByFallbacks(path, {
errors: parseResult.errors,
parsed: value
}, options);
parseResult.parsed = parsedResultsAfterFallbacks.parsed;
parseResult.errors = (parseResult.errors || []).concat(parsedResultsAfterFallbacks.errors || []);
if (this.__beforeValidationCallbacks.size > 0) {
for (const callback of this.__beforeValidationCallbacks.values()) {
const parsedValuesAfterValidationCallbacks = await callback(adapterToUse, adapterToUse[schemaAdapterFieldType], this, this.__transformedSchemas[adapterToUse.constructor.name].schemas, value, path, options);
parseResult.parsed = parsedValuesAfterValidationCallbacks.parsed;
parseResult.errors = Array.isArray(parseResult.errors) && Array.isArray(parsedValuesAfterValidationCallbacks.errors) ? [
...parseResult.errors,
...parsedValuesAfterValidationCallbacks.errors
] : Array.isArray(parseResult.errors) ? parseResult.errors : parsedValuesAfterValidationCallbacks.errors;
}
} else {
const parsedValuesAfterValidatingByAdapter = await this.__validateByAdapter(adapterToUse, adapterToUse[schemaAdapterFieldType], this.__transformedSchemas[adapterToUse.constructor.name].schemas[0], value, path, options);
parseResult.parsed = parsedValuesAfterValidatingByAdapter.parsed;
parseResult.errors = (parseResult.errors || []).concat(parsedValuesAfterValidatingByAdapter.errors);
}
const hasToInternalCallback = typeof this.__toInternal === "function";
const shouldCallToInternalDuringParse = hasToInternalCallback && (options.toInternalToBubbleUp?.length === 0 || Array.isArray(options.toInternalToBubbleUp) === false);
const hasNoErrors = parseResult.errors === void 0 || (parseResult.errors || []).length === 0;
await Promise.all(this.__refinements.map(async (refinement) => {
const errorOrNothing = await Promise.resolve(refinement({
value: parseResult.parsed,
context: options.context
}));
if (typeof errorOrNothing === "undefined") return;
parseResult.errors.push({
isValid: false,
code: errorOrNothing.code,
message: errorOrNothing.message,
received: parseResult.parsed,
path
});
}));
if (shouldCallToInternalDuringParse && hasNoErrors) parseResult.parsed = await this.__toInternal(value);
if (shouldRunToInternalToBubbleUp && hasNoErrors) for (const functionToModifyResult of options.toInternalToBubbleUp || []) await functionToModifyResult();
return parseResult;
}
/**
* This let's you refine the schema with custom validations. This is useful when you want to validate something
* that is not supported by default by the schema adapter.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().refine((value) => {
* if (value < 0) return { code: 'invalid_number', message: 'The number should be greater than 0' };
* });
*
* const { errors, parsed } = await numberSchema.parse(-1);
*
* console.log(errors);
* // [{ isValid: false, code: 'invalid_number', message: 'The number should be greater than 0', path: [] }]
* ```
*
* @param refinementCallback - The callback that will be called to validate the value.
*/
refine(refinementCallback) {
this.__refinements.push(refinementCallback);
return this;
}
/**
* Allows the value to be either undefined or null.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().optional();
*
* const { errors, parsed } = await numberSchema.parse(undefined);
*
* console.log(parsed); // undefined
*
* const { errors, parsed } = await numberSchema.parse(null);
*
* console.log(parsed); // null
*
* const { errors, parsed } = await numberSchema.parse(1);
*
* console.log(parsed); // 1
* ```
*
* @returns - The schema we are working with.
*/
optional(options) {
this.__optional = {
message: typeof options?.message === "string" ? options.message : "Required",
allow: typeof options?.allow === "boolean" ? options.allow : true
};
return this;
}
/**
* Allows the value to be null and ONLY null. You can also use this function to set a custom message when
* the value is NULL by setting the { message: 'Your custom message', allow: false } on the options.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().nullable();
*
* const { errors, parsed } = await numberSchema.parse(null);
*
* console.log(parsed); // null
*
* const { errors, parsed } = await numberSchema.parse(undefined);
*
* console.log(errors); // [{ isValid: false, code: 'invalid_type', message: 'Invalid type', path: [] }]
* ```
*
* @param options - The options for the nullable function.
* @param options.message - The message to be shown when the value is not null. Defaults to 'Cannot be null'.
* @param options.allow - Whether the value can be null or not. Defaults to true.
*
* @returns The schema.
*/
nullable(options) {
this.__nullable = {
message: typeof options?.message === "string" ? options.message : "Cannot be null",
allow: typeof options?.allow === "boolean" ? options.allow : true
};
return this;
}
/**
* Appends a custom schema to the schema, this way it will bypass the creation of the schema in runtime.
*
* By default when validating, on the first validation we create the schema. Just during the first validation.
* With this function, you bypass that, so you can speed up the validation process.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
* import * as z from 'zod';
*
* const numberSchema = p.number().appendSchema(z.number());
*
* const { errors, parsed } = await numberSchema.parse(1);
* ```
*
* @param schema - The schema to be appended.
* @param args - The arguments for the schema.
* @param args.adapter - The adapter to be used. If not provided, the default adapter will be used.
*
* @returns The same schema again.
*/
appendSchema(schema2, args) {
const adapter = args?.adapter || getDefaultAdapter();
this.__transformedSchemas[adapter.constructor.name] = {
transformed: true,
adapter,
schemas: [
schema2
]
};
return this;
}
/**
* This method will remove the value from the representation of the schema. If the value is undefined it will keep
* that way otherwise it will set the value to undefined after it's validated.
* This is used in conjunction with the {@link data} function, the {@link parse} function or {@link validate}
* function. This will remove the value from the representation of the schema.
*
* By default, the value will be removed just from the representation, in other words, when you call the {@link data}
* function. But if you want to remove the value from the internal representation, you can pass the argument
* `toInternal` as true. Then if you still want to remove the value from the representation, you will need to pass
* the argument `toRepresentation` as true as well.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const userSchema = p.object({
* id: p.number().optional(),
* name: p.string(),
* password: p.string().omit()
* });
*
* const user = await userSchema.data({
* id: 1,
* name: 'John Doe',
* password: '123456'
* });
*
* console.log(user); // { id: 1, name: 'John Doe' }
* ```
*
*
* @param args - By default, the value will be removed just from the representation, in other words, when you call
* the {@link data} function.
* But if you want to remove the value from the internal representation, you can pass the argument `toInternal`
* as true. Then if you still want to remove the value from the representation, you will need to pass the
* argument `toRepresentation` as true as well.
*
* @returns The schema.
*/
omit(args) {
const toRepresentation = typeof args?.toRepresentation === "boolean" ? args.toRepresentation : typeof args?.toInternal !== "boolean";
const toInternal = typeof args?.toInternal === "boolean" ? args.toInternal : false;
if (toInternal) {
if (this.__toInternal) {
const toInternal2 = this.__toInternal;
this.__toInternal = async (value) => {
await toInternal2(value);
return void 0;
};
} else this.__toInternal = async () => void 0;
} else if (toRepresentation) {
if (this.__toRepresentation) {
const toRepresentation2 = this.__toRepresentation;
this.__toRepresentation = async (value) => {
await toRepresentation2(value);
return void 0;
};
} else this.__toRepresentation = async () => void 0;
}
return this;
}
/**
* This function is used in conjunction with the {@link validate} function. It's used to save a value to an external
* source like a database. You should always return the schema after you save the value, that way we will always have
* the correct type of the schema after the save operation.
*
* You can use the {@link toRepresentation} function to transform and clean the value it returns after the save.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* import { User } from './models';
*
* const userSchema = p.object({
* id: p.number().optional(),
* name: p.string(),
* email: p.string().email(),
* }).onSave(async (value) => {
* // Create or update the user on the database using palmares models or any other library of your choice.
* if (value.id)
* await User.default.set(value, { search: { id: value.id } });
* else
* await User.default.set(value);
*
* return value;
* });
*
*
* // Then, on your controller, do something like this:
* const { isValid, save, errors } = await userSchema.validate(req.body);
* if (isValid) {
* const savedValue = await save();
* return Response.json(savedValue, { status: 201 });
* }
*
* return Response.json({ errors }, { status: 400 });
* ```
*
* @param callback - The callback that will be called to save the value on an external source.
*
* @returns The schema.
*/
onSave(callback) {
this.__saveCallback = callback;
return this;
}
/**
* This function is used to validate the schema and save the value to the database. It is used in
* conjunction with the {@link onSave} function.
*
* Different from other validation libraries, palmares schemas is aware that you want to save. On your
* routes/functions we recommend to ALWAYS use this function instead of {@link parse} directly. This is because
* this function by default will return an object with the property `save` or the `errors`. If the errors are present,
* you can return the errors to the user. If the save property is present, you can use to save the value to an
* external source. e.g. a database.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* import { User } from './models';
*
* const userSchema = p.object({
* id: p.number().optional(),
* name: p.string(),
* email: p.string().email(),
* }).onSave(async (value) => {
* // Create or update the user on the database using palmares models or any other library of your choice.
* if (value.id)
* await User.default.set(value, { search: { id: value.id } });
* else
* await User.default.set(value);
*
* return value;
* });
*
*
* // Then, on your controller, do something like this:
* const { isValid, save, errors } = await userSchema.validate(req.body);
* if (isValid) {
* const savedValue = await save();
* return Response.json(savedValue, { status: 201 });
* }
*
* return Response.json({ errors }, { status: 400 });
* ```
*
* @param value - The value to be validated.
*
* @returns An object with the property isValid, if the value is valid, the function `save` will be present.
* If the value is invalid, the property errors will be present.
*/
async validate(value, context = {}) {
const { errors, parsed } = await this.__parse(value, [], {
context
});
if ((errors || []).length > 0) return {
isValid: false,
errors,
save: void 0
};
return {
isValid: true,
save: /* @__PURE__ */ __name(async () => this._save.bind(this)(parsed, context), "save"),
errors: void 0
};
}
/**
* Internal function, when we call the {@link validate} function it's this function that gets called
* when the user uses the `save` function returned by the {@link validate} function if the value is valid.
*
* @param value - The value to be saved.
*
* @returns The value to representation.
*/
async _save(value, context) {
if (this.__saveCallback) {
let result = this.__saveCallback(value);
if (typeof result === "function") result = result(context);
return this.data(result instanceof Promise ? await result : result);
}
return this.data(value);
}
/**
* This function is used to validate and parse the value to the internal representation of the schema.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().allowString();
*
* const { errors, parsed } = await numberSchema.parse('123');
*
* console.log(parsed); // 123
* ```
*
* @param value - The value to be parsed.
*
* @returns The parsed value.
*/
async parse(value) {
return this.__parse(value, [], {});
}
/**
* This function is used to transform the value to the representation without validating it.
* This is useful when you want to return a data from a query directly to the user. But for example
* you are returning the data of a user, you can clean the password or any other sensitive data.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const userSchema = p.object({
* id: p.number().optional(),
* name: p.string(),
* email: p.string().email(),
* password: p.string().optional()
* }).toRepresentation(async (value) => {
* return {
* id: value.id,
* name: value.name,
* email: value.email
* }
* });
*
* const user = await userSchema.data({
* id: 1,
* name: 'John Doe',
* email: 'john@gmail.com',
* password: '123456'
* });
* ```
*/
async data(value) {
this.__getDefaultTransformedSchemas();
if (typeof this.__runBeforeParseAndData === "function") await this.__runBeforeParseAndData(this);
value = await this.__parsersToTransformValue(value);
if (this.__toRepresentation) value = await Promise.resolve(this.__toRepresentation(value));
if (this.__defaultFunction && value === void 0) value = await Promise.resolve(this.__defaultFunction());
return value;
}
instanceOf(args) {
this.__type.check = typeof args.check === "function" ? args.check : this.__type.check;
this.__type.message = typeof args.message === "string" ? args.message : this.__type.message;
return this;
}
/**
* This function is used to add a default value to the schema. If the value is either undefined or null,
* the default value will be used.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().default(0);
*
* const { errors, parsed } = await numberSchema.parse(undefined);
*
* console.log(parsed); // 0
* ```
*/
default(defaultValueOrFunction) {
const isFunction = typeof defaultValueOrFunction === "function";
if (isFunction) this.__defaultFunction = defaultValueOrFunction;
else this.__defaultFunction = async () => defaultValueOrFunction;
return this;
}
/**
* This function let's you customize the schema your own way. After we translate the schema on the adapter we call
* this function to let you customize the custom schema your own way. Our API does not support passthrough?
* No problem, you can use this function to customize the zod schema.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const numberSchema = p.number().extends((schema) => {
* return schema.nonnegative();
* });
*
* const { errors, parsed } = await numberSchema.parse(-1);
*
* console.log(errors);
* // [{ isValid: false, code: 'nonnegative', message: 'The number should be nonnegative', path: [] }]
* ```
*
* @param callback - The callback that will be called to customize the schema.
* @param toStringCallback - The callback that will be called to transform the schema to a string when you want
* to compile the underlying schema to a string so you can save it for future runs.
*
* @returns The schema.
*/
extends(callback, toStringCallback) {
this.__extends = {
callback,
toStringCallback
};
return this;
}
/**
* This function is used to transform the value to the representation of the schema. When using the {@link data}
* function. With this function you have full control to add data cleaning for example, transforming the data
* and whatever. Another use case is when you want to return deeply nested recursive data.
* The schema maps to itself.
*
* @example
* ```typescript
* import * as p from '@palmares/schemas';
*
* const recursiveSchema = p.object({
* id: p.number().optional(),
* name: p.string(),
* }).toRepresentation(async (value) => {
* return {
* id: value.id,
* name: value.name,
* children: await Promise.all(value.children.map(async (child) => await recursiveSchema.data(child)))
* }
* });
*
* const data = await recursiveSchema.data({
* id: 1,
* name: 'John Doe',
* });
* ```
*
* @example
* ```
* import * as p from '@palmares/schemas';
*
* const colorToRGBSchema = p.string().toRepresentation(async (value) => {
* switch (value) {
* case 'red': return { r: 255, g: 0, b: 0 };
* case 'g