Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TS error when using custom() #487

Closed
ruiaraujo012 opened this issue Mar 18, 2024 · 28 comments
Closed

TS error when using custom() #487

ruiaraujo012 opened this issue Mar 18, 2024 · 28 comments
Assignees
Labels
enhancement New feature or request

Comments

@ruiaraujo012
Copy link

The property schema: 'object' gives me a TS error and I can't put it to work.

const FormSchema = v.object(
  {
    dataFimValidadeFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataFimValidadeInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataInicioValidadeFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataInicioValidadeInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataPedidoFim: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    dataPedidoInicio: v.nullable(
      v.optional(v.coerce(v.date('validation.date'), (input) => new Date(input as number | string | Date))),
    ),
    idEstado: v.optional(v.coerce(v.string(), String)),
    idInterno: v.optional(v.coerce(v.string(), String)),
    identificador: v.optional(v.string()),
    nome: v.optional(v.string()),
  },
  [
    v.custom((inputs) => {
      if (inputs.dataInicioValidadeInicio && inputs.dataInicioValidadeFim) {
        if (dateRangeValidator(inputs.dataInicioValidadeInicio, inputs.dataInicioValidadeFim)) {
          throw new ValiError([
            {
              input: inputs.dataInicioValidadeInicio,
              message: 'validation.minDateExceeded',
              origin: 'value',
              reason: 'date',
              validation: 'custom',
              path: [
                {
                  input: inputs,
                  key: 'dataInicioValidadeInicio',
                  schema: 'object', <--- Object literal may only specify known properties, and 'schema' does not exist in type 'PathItem'.
                  value: inputs.dataInicioValidadeInicio,
                },
              ],
            },
          ]);
        }
      }

      return true;
    }),
  ],
);
@fabian-hiller
Copy link
Owner

fabian-hiller commented Mar 19, 2024

Don't throw inside of custom. Instead always return a boolean: https://valibot.dev/api/custom/

You can forward issues with this method: https://valibot.dev/api/forward/

@fabian-hiller fabian-hiller self-assigned this Mar 19, 2024
@fabian-hiller fabian-hiller added the question Further information is requested label Mar 19, 2024
@ruiaraujo012
Copy link
Author

Hum, I saw some issues where people were throwing inside custom.

Imagine this scenario, how can I achieve the same result without throwing in custom?

codOutrasNacionalidades: v.array(v.string([v.minLength(1, 'required.otherNationalities')]), [
    v.minLength(1, 'required.otherNationalities'),
    v.custom((input) => {
      const duplicates = input.filter((item, index) => input.indexOf(item) !== index);

      const issues: v.SchemaIssues = [] as unknown as v.SchemaIssues;

      if (duplicates.length > 0) {
        input.forEach((val, index) => {
          if (duplicates.includes(val)) {
            issues.push(
              createValibotIssue({
                fieldPath: `pessoa.codOutrasNacionalidades.${index}`,
                input: { 'pessoa.codOutrasNacionalidades': input },
                message: 'validation.uniqueNationalities',
                reason: 'array',
              }),
            );
          }
        });
      }

      if (issues.length > 0) {
        throw new v.ValiError(issues);
      }

      return true;
    }),
  ]),
export const createValibotIssue = ({
  message,
  reason,
  input,
  fieldPath,
}: Pick<SchemaIssue, 'message' | 'reason'> & { input: Record<string, unknown>; fieldPath: string }): SchemaIssue => ({
  context: '',
  expected: null,
  input: input[fieldPath],
  message,
  path: [
    {
      input,
      key: fieldPath,
      origin: 'value',
      type: 'object',
      value: input[fieldPath],
    },
  ],
  reason,
  received: '',
});

@fabian-hiller
Copy link
Owner

I would return false instead of throwing and error and add the error message as the second parameter to custom. But currently there is no way to return multiple issues, but I am in the process of rewriting Valibot and it will be easier then.

@fabian-hiller
Copy link
Owner

fabian-hiller commented Mar 20, 2024

Also we have a some and every validation action specifically for arrays. You could use every to check for duplicate values.

@ruiaraujo012
Copy link
Author

I would return false instead of throwing and error and add the error message as the second parameter to custom. But currently there is no way to return multiple issues, but I am in the process of rewriting Valibot and it will be easier then.

I've seen the discussion, I'm waiting for that :).

Also we have a some and every validation action specifically for arrays. You could use every to check for duplicate values.

I didn't try it yet, but do every put the error at the index of the element?

@fabian-hiller
Copy link
Owner

I didn't try it yet, but do every put the error at the index of the element?

No, but that's an interesting idea!

I've seen the discussion, I'm waiting for that

With my current code pipeline actions accept a dataset as the first argument and return a dataset as a result. This will allow you to easily write your own actions and add as many issues as you want. I plan to publish a first draft next week.

@ruiaraujo012
Copy link
Author

ruiaraujo012 commented Mar 20, 2024

No, but that's an interesting idea!

I think so, in my current implementation, this is important to select the duplicated option, and with the code that I've shared here, it works.
image

With my current code pipeline actions accept a dataset as the first argument and return a dataset as a result. This will allow you to easily write your own actions and add as many issues as you want. I plan to publish a first draft next week.

Those are great news, I'm looking forward to testing it.

@fabian-hiller
Copy link
Owner

I plan to publish a blog post in two or three weeks about the results and investigations of the new pipe function implementation as well as the rewrite of the library. For now, I expect the bundle size to be smaller, the performance to be faster and the types to be more accurate.

@ruiaraujo012
Copy link
Author

Although it's not recommended, I'll let v.custom with throw as it is working as I need, I'll come here again when the rewrite is done to update it. And perhaps close the issue then with a solution.

In summary, I would like to have:

  • An option to validate every element of the array and add the issue to the index of the occurrence (in this example, otherNationalities.2.
  • An option to add issues globally to the schema based on field relations:
const Schema = v.object(
  {
     mainNationality: v.string([v.minLength(1, 'required.nationality')]),
     otherNationalities: v.array(v.string([v.minLength(1, 'required.otherNationalities')]), [
        v.minLength(1, 'required.otherNationalities'),
        (custom valiration to check duplicated)
     ])
    (...other fields)
  },
  [
    v.<someMethod>((inputs) => {
      // If some element in otherNationalities is equal to the mainNationality, 
      // add an issue to the otherNationalities item index
    }),
  ],
);

@fabian-hiller fabian-hiller added enhancement New feature or request and removed question Further information is requested labels Mar 22, 2024
@ruiaraujo012
Copy link
Author

Hi @fabian-hiller I saw your comment on the rewrite PR, I was thinking about testing the new implementation of pipe in this context, do you have any suggestion on where I can start? Or there are no way to do this yet?

@fabian-hiller
Copy link
Owner

It is possible, but you have to write your own action. The concept is very simple. Here is an example where I have called the action "items":

import {
  _addIssue,
  type BaseIssue,
  type BaseValidation,
  type ErrorMessage,
} from 'valibot';

/**
 * Items issue type.
 */
export interface ItemsIssue<TInput extends unknown[]>
  extends BaseIssue<TInput> {
  /**
   * The issue kind.
   */
  readonly kind: 'validation';
  /**
   * The issue type.
   */
  readonly type: 'items';
  /**
   * The expected input.
   */
  readonly expected: null;
  /**
   * The validation function.
   */
  readonly requirement: (input: TInput[number]) => boolean;
}

/**
 * Items action type.
 */
export interface ItemsAction<
  TInput extends unknown[],
  TMessage extends ErrorMessage<ItemsIssue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, ItemsIssue<TInput>> {
  /**
   * The action type.
   */
  readonly type: 'items';
  /**
   * The action reference.
   */
  readonly reference: typeof items;
  /**
   * The expected property.
   */
  readonly expects: null;
  /**
   * The validation function.
   */
  readonly requirement: (input: TInput[number]) => boolean;
  /**
   * The error message.
   */
  readonly message: TMessage;
}

/**
 * Creates an items validation action.
 *
 * @param requirement The validation function.
 *
 * @returns An items action.
 */
export function items<TInput extends unknown[]>(
  requirement: (input: TInput[number]) => boolean
): ItemsAction<TInput, undefined>;

/**
 * Creates an items validation action.
 *
 * @param requirement The validation function.
 * @param message The error message.
 *
 * @returns An items action.
 */
export function items<
  TInput extends unknown[],
  const TMessage extends ErrorMessage<ItemsIssue<TInput>> | undefined,
>(
  requirement: (input: TInput[number]) => boolean,
  message: TMessage
): ItemsAction<TInput, TMessage>;

export function items(
  requirement: (input: unknown) => boolean,
  message?: ErrorMessage<ItemsIssue<unknown[]>>
): ItemsAction<unknown[], ErrorMessage<ItemsIssue<unknown[]>> | undefined> {
  return {
    kind: 'validation',
    type: 'items',
    reference: items,
    async: false,
    expects: null,
    requirement,
    message,
    _run(dataset, config) {
      if (dataset.typed) {
        for (let index = 0; index < dataset.value.length; index++) {
          const item = dataset.value[index];
          if (!this.requirement(item)) {
            _addIssue(this, 'item', dataset, config, {
              input: item,
              path: [
                {
                  type: 'array',
                  origin: 'value',
                  input: dataset.value,
                  key: index,
                  value: item,
                },
              ],
            });
          }
        }
      }
      return dataset;
    },
  };
}

Now you can write the following code:

import * as v from 'valibot';
import { items } from './your-path';

const Schema = v.pipe(
  v.array(v.string()),
  items((item) => item.startsWith('foo'), 'Your message')
);

@ruiaraujo012
Copy link
Author

ruiaraujo012 commented May 10, 2024

Thanks. I'll try that, do you plan on implementing it in valibot?

@fabian-hiller
Copy link
Owner

Yes, I will consider it. Do you have some good alternatives names for the action?

@ruiaraujo012
Copy link
Author

The only one that I could think of is iterate.

@ruiaraujo012
Copy link
Author

ruiaraujo012 commented May 10, 2024

I was thinking, it would be nice if the callback had 2 or 3 arguments (item, index, items) => boolean.

With that we could create validations based on other items, for example, identify duplicated values.

items((item, index, items) => items.filter((v)=> v===item).length > 1, 'Duplicated')

Let me know what you think.

Another requirement I can think that would be great is an action to validate properties based on other properties. Let me give you an example:

{
    "primaryEmail": "[email protected]",
    "otherEmails": ["[email protected]", "[email protected]"]
}

To validate that the primaryEmail is not present in otherEmails the items action doesn't work, I need some actions that allow me to add issues globally.

I hope I didn't confuse you.

@fabian-hiller
Copy link
Owner

The only one that I could think of is iterate.

Thanks! Will think about it! Both names could work.

I was thinking, it would be nice if the callback had 2 or 3 arguments (item, index, items) => boolean.

I agree!

To validate that the primaryEmail is not present in...

This requires a custom validation in the pipeline of the object definition with a forward to the appropriate field. Note: We renamed custom to check with the rewrite.

@ruiaraujo012
Copy link
Author

Oh, that's what I need, I thought that the custom was deprecated. Thanks. I'll play a bit with the current state of the lib.

Please update this issue when this action gets done.

@ruiaraujo012
Copy link
Author

ruiaraujo012 commented May 11, 2024

@fabian-hiller I was playing around and found that the forward action is missing.

@fabian-hiller
Copy link
Owner

You are right. I will probably add it today or tomorrow. I will ping you then.

@fabian-hiller
Copy link
Owner

I am working on it and will probably add it later today.

@fabian-hiller
Copy link
Owner

Done

@ruiaraujo012
Copy link
Author

Thanks, I'll play with it again.

@fabian-hiller
Copy link
Owner

Is this issue resolved? I will close it for now, but will reopen it if not.

@fabian-hiller
Copy link
Owner

Feel free to create a new issue (or PR) to add an items action in the next weeks.

@ruiaraujo012
Copy link
Author

Is this issue resolved? I will close it for now, but will reopen it if not.

I'll check it ASAP and let you know.

Feel free to create a new issue (or PR) to add an items action in the next weeks.

I'll open one issue latter today.

@ruiaraujo012
Copy link
Author

ruiaraujo012 commented May 23, 2024

I'm not able to put a schema to work with this requirement, can you help here?

{
    "primaryEmail": "[email protected]",
    "otherEmails": ["[email protected]", "[email protected]"]
}

To validate that the primaryEmail is not present in otherEmails the items action doesn't work, I need some actions that allow me to add issues globally.

The forward action doesn't work because I cannot put the key and index of the duplicated email, in this case otherEmails.1.

One possible solution that came to my mind is a method that allow us to add issues to the list of issues already created by valibot validations. Something like:

v.someMethod((inputs, ctx) => {
// ...(my custom validation with ctx.addIssue())...
return ctx.issues // Or other type of return
}) 

@fabian-hiller
Copy link
Owner

fabian-hiller commented May 23, 2024

Maybe we should add an advanced refine method (similar to Zod's superRefine) that allows you to directly interact with the input and output dataset of the method and not only with the raw value.

@fabian-hiller fabian-hiller reopened this May 23, 2024
@ruiaraujo012
Copy link
Author

I agree, let's do the following, I'll close this issue and open a new one with a recap of this one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants