longshotlabs / simpl-schema

A JavaScript schema validation package that supports direct validation of MongoDB update modifier objects
https://www.npmjs.com/package/simpl-schema
MIT License
560 stars 114 forks source link

Customize validation message example + documentation issues #73

Closed ZhuQinglei closed 7 years ago

ZhuQinglei commented 7 years ago

@aldeed I am using this for my meteor project and I have migrate from meteor package to this node_module package together with collection-2-core package.

Currently I want to customize the validation messages for regEx because all error message for regEx is just "failed regular expression validation", and I would like to give a more clear validation message such as "must contain at least 1 digit" etc.

However, I couldn't find a correct way/clear example to do it. I checked the message-box package and still don't know how to do it, and I found the documentation a bit confusing :

  1. the CHANGELOG in this README is not found
  2. at custom validation, it mentions using message-box but doen's show clearly how to use it, I am still very new to Meteor and npm so may need more explanation on how to use it.
  3. at validate one key against another, it is using SimpleSchema.messages again instead of message-box which confuse me that which is the correct way to do.
  4. in message-box package, I am not sure where should I call the MessageBox.defaults() to make it effective (in import/ client folder?), also theimport MessageBox from 'message-box'; should be included in the Usage.
  5. when I tried to follow the Manually Adding a Validation Error in imports/collections/Users.js with
    
    SimpleSchema.messageBox.messages({
    en: {
        wrongPassword: "Wrong password"
    }
    });
It shows me messageBox is undefined even if I have imported both simpleschema and message-box npm packages.
![screen shot 2017-03-10 at 6 29 52 pm](https://cloud.githubusercontent.com/assets/16860302/23792063/6b3337c6-05c0-11e7-9bdb-9504b6715db9.png)

I tried to customize with the code below: 

MessageBox.defaults({ initialLanguage: 'en', messages: { en: { required: '{{label}} is required', minString: '{{label}} must be at least {{min}} characters', maxString: '{{label}} cannot exceed {{max}} characters', minNumber: '{{label}} must be at least {{min}}', maxNumber: '{{label}} cannot exceed {{max}}', minNumberExclusive: '{{label}} must be greater than {{min}}', maxNumberExclusive: '{{label}} must be less than {{max}}', minDate: '{{label}} must be on or after {{min}}', maxDate: '{{label}} cannot be after {{max}}', badDate: '{{label}} is not a valid date', minCount: 'You must specify at least {{minCount}} values', maxCount: 'You cannot specify more than {{maxCount}} values', noDecimal: '{{label}} must be an integer', notAllowed: '{{value}} is not an allowed value', expectedType: '{{label}} must be of type {{dataType}}', regEx: function ({ label, type, regExp, }) { console.log(label, type, regExp) // See if there's one where exp matches this expression let msgObj; if (regExp) { msgObj = _.find(regExpMessages, (o) => o.exp && o.exp.toString() === regExp);

            }
            var regExpMessageTail = 'failed regular expression validation';
            if (regExp === /\d/) { // check for at least 1 digit
                regExpMessageTail = 'must contain at least 1 digit';
            }
            var regExpMessage = msgObj ? msgObj.msg : regExpMessageTail;

            return `${label} ${regExpMessage}`;
        },
        keyNotInSchema: '{{name}} is not allowed by the schema',
    },
}

});


But it is not working. I understand that it is still in progress, but would be nice to add a clear example on customize validation messages for regEx. Thank you!
Szayet commented 7 years ago
  1. You are correct, here is the mentioned changelog.
  2. I hope I can help in it.
  3. In my humble opinion that was the closest try of yours. It could work if you extend the MessageBox.defaults() or the specific schema's messagebox with instance.messageBox.messages() with the wrongPassword code you provided before:
    import MessageBox from 'message-box';
    MessageBox.defaults({
    en: {
    wrongPassword: "Wrong password"
    }
    })

    Then add a custom() validation error (like here) to your Schema that can throw your newly added error code:

    
    custom: function () {
    var reg = new RegExp('ab+c'); //Your custom regular expression
    if (!reg.test(this.value)) {
     return "wrongPassword";
    }
    }

4. These packages are newish and @aldeed working hard on it for us. So the documentations and functionalities can be a bit confusing 😄 
5. `Schema.messageBox` is NOT (yet #77) global ([connecting issue for the same problem](https://github.com/aldeed/node-message-box/issues/6)). The documentation is wrong in that case. You can only get / set it for singular `SimpeSchema` instances.
The included `MessageBox.defaults` code you provided contains `simpl-schema` specific `regExpObj` and `regExpMessages` so it won't work because of that.
ZhuQinglei commented 7 years ago

@Szayet Thank you soooooo much for the help and reply!!

According to your instruction, I can call the custom validation method and get return value as I expected, however, the validation error message is not passed to client side to display.

Here is my code in imports/Users.js:

MessageBox.defaults({
    en: {
        EmailNotUnique: "The email address is already taken"
    }
});
Meteor.users.schema = new SimpleSchema({
    _id: {type: String, optional: true},
    emails: {type: Array},
    'emails.$': {type: Object, label: 'Email'},
    'emails.$.address': {
        type: String, label: 'Email', max: 255, regEx: SimpleSchema.RegEx.Email,
        custom() {
            if (Meteor.isClient && this.isSet) {
                Meteor.call("accountsIsEmailAvailable", this.value, (error, result) => {
                    console.log(error, result);
                    if (result) {
                        this.validationContext.addValidationErrors([{
                            name: "email",
                            type: "EmailNotUnique"
                        }]);
                    }
                });
            }
        }
    },
    'emails.$.verified': {type: Boolean},
    createdAt: {type: Date, optional: true},
    services: {type: Object, blackbox: true, optional: true},
    username: {type: String, optional: true},
    profile: {type: UserProfile, optional: true},
});

the meteor mtd 'accountsIsEmailAvailable ' is using Accounts.findUserByEmail(email) in Accounts package and returns the user obj if it finds the email already existing in db.

And here is my console log and message print screen shot 2017-03-13 at 3 43 40 pm at client side:

screen shot 2017-03-13 at 3 37 59 pm

I can see that the custom function is called async at end of my redux dispatch error messages. But I would like to add this email error to the client side for displaying, do you have any suggestion on that?

Yes, I fully understand that the package is still newish and it is difficult to manage so many useful & popular packages at the same time! Really appreciate all your hard work and the convenience that @aldeed 's packages offer.

Szayet commented 7 years ago

@ZhuQinglei No problem, I'm glad it helped. 😄

To display async validation errors, you have to use Tracker support. Here is the relevant part of the documentation.

import { Tracker } from 'meteor/tracker';
new SimpleSchema({
  ....
}, { tracker: Tracker });

Then your ValidationContext#validationErrors should rerun if placed inside a Tracker watched part. (like in the linked example above). To save the hassle aldeed:autoform forms can handle this kind of error display which package I higly recommend using. That package's documentation also points out to pass the Tracker.

nosizejosh commented 7 years ago

@Szayet Thank you for helping. I followed your comments to get me out of a tough spot. Though my code is working now, I just cant get the error message to display the correct message. Please check my code and tell me where I am going wrong.

> MessageBox.defaults({
>   en: {
>     ShareLimitExceeded: "Number of shares issued cannot exceed share cap"
>   }
> });
> 
> Shares.schema = new SimpleSchema({
>   description: {
>     type: String,
>     label: 'Share Description',
>     max: 1000,
>     min: 1,
>   },
>   issuedShares: {
>     type: Number,
>     label: 'Shares to Issue',
>     min: 1,
>     custom: function () { // call a server method to compare this value against sharecap
>       if (Meteor.isClient) {
>         Meteor.call("ShareBank.methods.shareCapExceeded", this.value, (error, result) => {
>             console.log(error, result);
>             if (error) {
>                 this.validationContext.addValidationErrors([{
>                     name: "issuedShares",
>                     type: "ShareLimitExceeded"
>                 }]);
>             }
>         });
>       }
>     }
>   },

I keep getting "issuedShares is invalid" as error message instead of "Number of shares issued cannot exceed share cap" as required from

ShareLimitExceeded: "Number of shares issued cannot exceed share cap"

unknown4unnamed commented 7 years ago

@nosizejosh Hello, recently I fall into the same problem. And I have solved it in fork way. link

Maybe it can helps somebody.

ghost commented 7 years ago

I had a similar problem, I wanted to replace the built-in errors with translated versions:

import SimpleSchema from 'simpl-schema'
import MessageBox from 'message-box'
import { Tracker } from 'meteor/tracker'

const mySchema = new SimpleSchema({
  ...
}, {tracker: Tracker})

// this domain regex matches all domains that have at least one .
// sadly IPv4 Adresses will be caught too but technically those are valid domains
// this expression is extracted from the original RFC 5322 mail expression
// a modification enforces that the tld consists only of characters
const rxDomain = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z](?:[a-z-]*[a-z])?'
// this domain regex matches everythign that could be a domain in intranet
// that means "localhost" is a valid domain
const rxNameDomain = '(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.|$))+'
// strict IPv4 expression which allows 0-255 per oktett
const rxIPv4 = '(?:(?:[0-1]?\\d{1,2}|2[0-4]\\d|25[0-5])(?:\\.|$)){4}'
// strict IPv6 expression which allows (and validates) all shortcuts
const rxIPv6 = '(?:(?:[\\dA-Fa-f]{1,4}(?::|$)){8}' // full adress
  + '|(?=(?:[^:\\s]|:[^:\\s])*::(?:[^:\\s]|:[^:\\s])*$)' // or min/max one '::'
  + '[\\dA-Fa-f]{0,4}(?:::?(?:[\\dA-Fa-f]{1,4}|$)){1,6})' // and short adress
// this allows domains (also localhost etc) and ip adresses
const rxWeakDomain = `(?:${[rxNameDomain, rxIPv4, rxIPv6].join('|')})`

const regExpObj = {
  // We use the RegExp suggested by W3C in http://www.w3.org/TR/html5/forms.html#valid-e-mail-address
  // This is probably the same logic used by most browsers when type=email, which is our goal. It is
  // a very permissive expression. Some apps may wish to be more strict and can write their own RegExp.
  Email: /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/,
  // Like Email but requires the TLD (.com, etc)
  EmailWithTLD: /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+\/=?^_`{|}~-]+)*@(?:[A-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[A-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?$/,

  Domain: new RegExp(`^${rxDomain}$`),
  WeakDomain: new RegExp(`^${rxWeakDomain}$`),

  IP: new RegExp(`^(?:${rxIPv4}|${rxIPv6})$`),
  IPv4: new RegExp(`^${rxIPv4}$`),
  IPv6: new RegExp(`^${rxIPv6}$`),
  // URL RegEx from https://gist.github.com/dperini/729294
  // http://mathiasbynens.be/demo/url-regex
  Url: /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a>
  // unique id from the random package also used by minimongo
  // character list: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L88
  // string length: https://github.com/meteor/meteor/blob/release/0.8.0/packages/random/random.js#L143
  Id: /^[23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz]{17}$/,
  // allows for a 5 digit zip code followed by a whitespace or dash and then 4 more digits
  // matches 11111 and 11111-1111 and 11111 1111
  ZipCode: /^\d{5}(?:[-\s]\d{4})?$/,
  // taken from Google's libphonenumber library
  // https://github.com/googlei18n/libphonenumber/blob/master/javascript/i18n/phonenumbers/phonenumberutil.js
  // reference the VALID_PHONE_NUMBER_PATTERN key
  // allows for common phone number symbols including + () and -
  Phone: /^[0-90-9٠-٩۰-۹]{2}$|^[++]*(?:[-x‐-―−ー--/  ­ <200b>⁠ ()()[].\[\]/~⁓∼ ~*]*[0-90-9٠-٩۰-۹]){3,}[-x‐-―−ー--/  ­ <200b>⁠ ()()[].\[\]/~⁓∼ ~0-90-9٠-٩۰-۹]*(?:ext=([0-90-9٠-٩۰-۹]{1,7})|[  \t,]*(?:e?xt(?:ensi(?:ó?|ó))?n?|e?xtn?|[,xx##~~]|int|anexo|int)[:\..]?[  \t,-]*([0-90-9٠-٩۰-۹]{1,7})#?|[- ]+([0-90-9٠-٩۰-۹]{1,5})#)?$/i, // eslint-disable->
}

const regExpMessages = [
  { exp: regExpObj.Email, msg: 'muss eine gültige Email-Adresse sein' },
  { exp: regExpObj.EmailWithTLD, msg: 'muss eine gültige Email-Adresse sein' },
  { exp: regExpObj.Domain, msg: 'muss eine gültige Domain sein' },
  { exp: regExpObj.WeakDomain, msg: 'muss eine gültige Domain sein' },
  { exp: regExpObj.IP, msg: 'muss eine gültige IPv4 or IPv6-Adresse sein' },
  { exp: regExpObj.IPv4, msg: 'muss eine gültige IPv4-Adresse sein' },
  { exp: regExpObj.IPv6, msg: 'muss eine gültige IPv6-Adresse sein' },
  { exp: regExpObj.Url, msg: 'muss eine gültige URL sein' },
  { exp: regExpObj.Id, msg: 'muss eine gültige alphanumerische ID sein' },
  { exp: regExpObj.ZipCode, msg: 'muss eine gültige Postleitzahl sein' },
  { exp: regExpObj.Phone, msg: 'muss eine gültige Telefonnummer sein' },
]

const myMessageBox = new MessageBox({
  initialLanguage: 'de',
  messages: {
    de: {
      required: '{{label}} wird benötigt',
      minString: '{{label}} muss mindestends {{min}} Buchstaben lang sein',
      maxString: '{{label}} darf nicht mehr als {{max}} lang sein',
      minNumber: '{{label}} muss mindestens {{min}} sein',
      maxNumber: '{{label}} darf nicht größer als {{max}} sein',
      minNumberExclusive: '{{label}} muss größer als {{min}} sein',
      maxNumberExclusive: '{{label}} muss kleiner als {{max}} sein',
      minDate: '{{label}} muss {{min}} oder später sein',
      maxDate: '{{label}} kann nicht nach {{max}} sein',
      badDate: '{{label}} ist kein gültiges Datum',
      minCount: 'Du musst mindestens {{minCount}} Werte angeben',
      maxCount: 'Du darfst nicht mehr als {{maxCount}} Werte angeben',
      noDecimal: '{{label}} muss eine ganze Zahl sein',
      notAllowed: '[value] ist kein erlaubter Wert',
      expectedType: '{{label}} muss vom Typ {{dataType}} sein',
      regEx({ label, regExp }) {
        // See if there's one where exp matches this expression
        let msgObj
        if (regExp) {
          msgObj = _.find(regExpMessages, (o) => o.exp && o.exp.toString() === regExp)
        }

        const regExpMessage = msgObj ? msgObj.msg : 'Die Eingabe wurde bei der Überprüfung durch einen regulären Ausdruck für ungültig erklärt'

        return `${label} ${regExpMessage}`
      },
      keyNotInSchema: '{{name}} ist nicht im Schema vorgesehen'
    }
  }
})

mySchema.messageBox = myMessageBox

Most code is taken from the messagebox defaults. Obviously you should put that messagebox in a file somewhere and import it always when you need it.

I wonder how this works: https://github.com/aldeed/node-simple-schema/blob/2b8f902e0d8d84eb923f9cf0cca2124f03f41cf8/lib/testHelpers/testSchema.js#L171

aldeed commented 7 years ago

https://github.com/aldeed/node-simple-schema#customizing-validation-messages

This was not very well documented until yesterday, but I'm pretty sure there's no actual issue here. If I'm wrong, I can reopen this.