UNPKG

@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,217 lines (1,208 loc) 228 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // 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 import { domain } from "@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 = 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 'green': return { r: 0, g: 255, b: 0 }; * case 'blue': return { r: 0, g: 0, b: 255 }; * default: return { r: 0, g: 0, b: 0 }; * } * }); * ``` * @param toRepresentationCallback - The callback that will be called to transform the value to the representation. * @param options - Options for the toRepresentation function. * @param options.after - Whether the toRepresentationCallback should be called after the existing * toRepresentationCallback. Defaults to true. * @param options.before - Whether the toRepresentationCallback should be called before the existing * toRepresentationCallback. Defaults to true. * * @returns The schema with a new return type */ toRepresentation(toRepresentationCallback, options) { if (this.__toRepresentation) { const before = typeof options?.before === "boolean" ? options.before : typeof options?.after === "boolean" ? !options.after : true; const existingToRepresentation = this.__toRepresentation; this.__toRepresentation = async (value) => { if (before) return toRepresentationCallback(await existingToRepresentation(value)); else return existingToRepresentation(await toRepresentationCallback(value)); }; } else this.__toRepresentation = toRepresentationCallback; return this; } /** * This function is used to transform the value to the internal representation of the schema. This is useful * when you want to transform the value to a type that the schema adapter can understand. For example, you * might want to transform a string to a date. This is the function you use. * * @example * ```typescript * import * as p from '@palmares/schemas'; * * const dateSchema = p.string().toInternal((value) => { * return new Date(value); * }); * * const date = await dateSchema.parse('2021-01-01'); * * console.log(date); // Date object * * const rgbToColorSchema = p.object({ * r: p.number().min(0).max(255), * g: p.number().min(0).max(255), * b: p.number().min(0).max(255), * }).toInternal(async (value) => { * if (value.r === 255 && value.g === 0 && value.b === 0) return 'red'; * if (value.r === 0 && value.g === 255 && value.b === 0) return 'green'; * if (value.r === 0 && value.g === 0 && value.b === 255) return 'blue'; * return `rgb(${value.r}, ${value.g}, ${value.b})`; * }); * ``` * * @param toInternalCallback - The callback that will be called to transform the value t