Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
882 lines
29 KiB
Text
Executable file
882 lines
29 KiB
Text
Executable file
'use strict';
|
|
|
|
const { assert, escapeRegex } = require('@hapi/hoek');
|
|
const { isDomainValid, isEmailValid, ipRegex, uriRegex } = require('@hapi/address');
|
|
const Tlds = require('@hapi/tlds');
|
|
|
|
const Any = require('./any');
|
|
const Common = require('../common');
|
|
|
|
|
|
const internals = {
|
|
tlds: Tlds.tlds instanceof Set ? { tlds: { allow: Tlds.tlds, deny: null } } : false, // $lab:coverage:ignore$
|
|
base64Regex: {
|
|
// paddingRequired
|
|
true: {
|
|
// urlSafe
|
|
true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}==|[\w\-]{3}=)?$/,
|
|
false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/
|
|
},
|
|
false: {
|
|
true: /^(?:[\w\-]{2}[\w\-]{2})*(?:[\w\-]{2}(==)?|[\w\-]{3}=?)?$/,
|
|
false: /^(?:[A-Za-z0-9+\/]{2}[A-Za-z0-9+\/]{2})*(?:[A-Za-z0-9+\/]{2}(==)?|[A-Za-z0-9+\/]{3}=?)?$/
|
|
}
|
|
},
|
|
dataUriRegex: /^data:[\w+.-]+\/[\w+.-]+;((charset=[\w-]+|base64),)?(.*)$/,
|
|
hexRegex: {
|
|
withPrefix: /^0x[0-9a-f]+$/i,
|
|
withOptionalPrefix: /^(?:0x)?[0-9a-f]+$/i,
|
|
withoutPrefix: /^[0-9a-f]+$/i
|
|
},
|
|
ipRegex: ipRegex({ cidr: 'forbidden' }).regex,
|
|
isoDurationRegex: /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/,
|
|
|
|
guidBrackets: {
|
|
'{': '}', '[': ']', '(': ')', '': ''
|
|
},
|
|
guidVersions: {
|
|
uuidv1: '1',
|
|
uuidv2: '2',
|
|
uuidv3: '3',
|
|
uuidv4: '4',
|
|
uuidv5: '5',
|
|
uuidv6: '6',
|
|
uuidv7: '7',
|
|
uuidv8: '8'
|
|
},
|
|
guidSeparators: new Set([undefined, true, false, '-', ':']),
|
|
|
|
normalizationForms: ['NFC', 'NFD', 'NFKC', 'NFKD']
|
|
};
|
|
|
|
|
|
module.exports = Any.extend({
|
|
|
|
type: 'string',
|
|
|
|
flags: {
|
|
|
|
insensitive: { default: false },
|
|
truncate: { default: false }
|
|
},
|
|
|
|
terms: {
|
|
|
|
replacements: { init: null }
|
|
},
|
|
|
|
coerce: {
|
|
from: 'string',
|
|
method(value, { schema, state, prefs }) {
|
|
|
|
const normalize = schema.$_getRule('normalize');
|
|
if (normalize) {
|
|
value = value.normalize(normalize.args.form);
|
|
}
|
|
|
|
const casing = schema.$_getRule('case');
|
|
if (casing) {
|
|
value = casing.args.direction === 'upper' ? value.toLocaleUpperCase() : value.toLocaleLowerCase();
|
|
}
|
|
|
|
const trim = schema.$_getRule('trim');
|
|
if (trim &&
|
|
trim.args.enabled) {
|
|
|
|
value = value.trim();
|
|
}
|
|
|
|
if (schema.$_terms.replacements) {
|
|
for (const replacement of schema.$_terms.replacements) {
|
|
value = value.replace(replacement.pattern, replacement.replacement);
|
|
}
|
|
}
|
|
|
|
const hex = schema.$_getRule('hex');
|
|
if (hex &&
|
|
hex.args.options.byteAligned &&
|
|
value.length % 2 !== 0) {
|
|
|
|
value = `0${value}`;
|
|
}
|
|
|
|
if (schema.$_getRule('isoDate')) {
|
|
const iso = internals.isoDate(value);
|
|
if (iso) {
|
|
value = iso;
|
|
}
|
|
}
|
|
|
|
if (schema._flags.truncate) {
|
|
const rule = schema.$_getRule('max');
|
|
if (rule) {
|
|
let limit = rule.args.limit;
|
|
if (Common.isResolvable(limit)) {
|
|
limit = limit.resolve(value, state, prefs);
|
|
if (!Common.limit(limit)) {
|
|
return { value, errors: schema.$_createError('any.ref', limit, { ref: rule.args.limit, arg: 'limit', reason: 'must be a positive integer' }, state, prefs) };
|
|
}
|
|
}
|
|
|
|
value = value.slice(0, limit);
|
|
}
|
|
}
|
|
|
|
return { value };
|
|
}
|
|
},
|
|
|
|
validate(value, { schema, error }) {
|
|
|
|
if (typeof value !== 'string') {
|
|
return { value, errors: error('string.base') };
|
|
}
|
|
|
|
if (value === '') {
|
|
const min = schema.$_getRule('min');
|
|
if (min &&
|
|
min.args.limit === 0) {
|
|
|
|
return;
|
|
}
|
|
|
|
return { value, errors: error('string.empty') };
|
|
}
|
|
},
|
|
|
|
rules: {
|
|
|
|
alphanum: {
|
|
method() {
|
|
|
|
return this.$_addRule('alphanum');
|
|
},
|
|
validate(value, helpers) {
|
|
|
|
if (/^[a-zA-Z0-9]+$/.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.alphanum');
|
|
}
|
|
},
|
|
|
|
base64: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['paddingRequired', 'urlSafe']);
|
|
|
|
options = { urlSafe: false, paddingRequired: true, ...options };
|
|
assert(typeof options.paddingRequired === 'boolean', 'paddingRequired must be boolean');
|
|
assert(typeof options.urlSafe === 'boolean', 'urlSafe must be boolean');
|
|
|
|
return this.$_addRule({ name: 'base64', args: { options } });
|
|
},
|
|
validate(value, helpers, { options }) {
|
|
|
|
const regex = internals.base64Regex[options.paddingRequired][options.urlSafe];
|
|
if (regex.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.base64');
|
|
}
|
|
},
|
|
|
|
case: {
|
|
method(direction) {
|
|
|
|
assert(['lower', 'upper'].includes(direction), 'Invalid case:', direction);
|
|
|
|
return this.$_addRule({ name: 'case', args: { direction } });
|
|
},
|
|
validate(value, helpers, { direction }) {
|
|
|
|
if (direction === 'lower' && value === value.toLocaleLowerCase() ||
|
|
direction === 'upper' && value === value.toLocaleUpperCase()) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return helpers.error(`string.${direction}case`);
|
|
},
|
|
convert: true
|
|
},
|
|
|
|
creditCard: {
|
|
method() {
|
|
|
|
return this.$_addRule('creditCard');
|
|
},
|
|
validate(value, helpers) {
|
|
|
|
let i = value.length;
|
|
let sum = 0;
|
|
let mul = 1;
|
|
|
|
while (i--) {
|
|
const char = value.charAt(i) * mul;
|
|
sum = sum + (char - (char > 9) * 9);
|
|
mul = mul ^ 3;
|
|
}
|
|
|
|
if (sum > 0 &&
|
|
sum % 10 === 0) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.creditCard');
|
|
}
|
|
},
|
|
|
|
dataUri: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['paddingRequired']);
|
|
|
|
options = { paddingRequired: true, ...options };
|
|
assert(typeof options.paddingRequired === 'boolean', 'paddingRequired must be boolean');
|
|
|
|
return this.$_addRule({ name: 'dataUri', args: { options } });
|
|
},
|
|
validate(value, helpers, { options }) {
|
|
|
|
const matches = value.match(internals.dataUriRegex);
|
|
|
|
if (matches) {
|
|
if (!matches[2]) {
|
|
return value;
|
|
}
|
|
|
|
if (matches[2] !== 'base64') {
|
|
return value;
|
|
}
|
|
|
|
const base64regex = internals.base64Regex[options.paddingRequired].false;
|
|
if (base64regex.test(matches[3])) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return helpers.error('string.dataUri');
|
|
}
|
|
},
|
|
|
|
domain: {
|
|
method(options) {
|
|
|
|
if (options) {
|
|
Common.assertOptions(options, ['allowFullyQualified', 'allowUnicode', 'allowUnderscore', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
|
|
}
|
|
|
|
const address = internals.addressOptions(options);
|
|
return this.$_addRule({ name: 'domain', args: { options }, address });
|
|
},
|
|
validate(value, helpers, args, { address }) {
|
|
|
|
if (isDomainValid(value, address)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.domain');
|
|
}
|
|
},
|
|
|
|
email: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['allowFullyQualified', 'allowUnicode', 'ignoreLength', 'maxDomainSegments', 'minDomainSegments', 'multiple', 'separator', 'tlds']);
|
|
assert(options.multiple === undefined || typeof options.multiple === 'boolean', 'multiple option must be an boolean');
|
|
|
|
const address = internals.addressOptions(options);
|
|
const regex = new RegExp(`\\s*[${options.separator ? escapeRegex(options.separator) : ','}]\\s*`);
|
|
|
|
return this.$_addRule({ name: 'email', args: { options }, regex, address });
|
|
},
|
|
validate(value, helpers, { options }, { regex, address }) {
|
|
|
|
const emails = options.multiple ? value.split(regex) : [value];
|
|
const invalids = [];
|
|
for (const email of emails) {
|
|
if (!isEmailValid(email, address)) {
|
|
invalids.push(email);
|
|
}
|
|
}
|
|
|
|
if (!invalids.length) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.email', { value, invalids });
|
|
}
|
|
},
|
|
|
|
guid: {
|
|
alias: 'uuid',
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['version', 'separator', 'wrapper']);
|
|
|
|
assert(
|
|
options.wrapper === undefined ||
|
|
typeof options.wrapper === 'boolean' ||
|
|
(typeof options.wrapper === 'string' && typeof internals.guidBrackets[options.wrapper] === 'string'),
|
|
`"wrapper" must be true, false, or one of "${Object.keys(internals.guidBrackets).filter(Boolean).join('", "')}"`
|
|
);
|
|
|
|
let versionNumbers = '';
|
|
|
|
if (options.version) {
|
|
const versions = [].concat(options.version);
|
|
|
|
assert(versions.length >= 1, 'version must have at least 1 valid version specified');
|
|
const set = new Set();
|
|
|
|
for (let i = 0; i < versions.length; ++i) {
|
|
const version = versions[i];
|
|
assert(typeof version === 'string', 'version at position ' + i + ' must be a string');
|
|
const versionNumber = internals.guidVersions[version.toLowerCase()];
|
|
assert(versionNumber, 'version at position ' + i + ' must be one of ' + Object.keys(internals.guidVersions).join(', '));
|
|
assert(!set.has(versionNumber), 'version at position ' + i + ' must not be a duplicate');
|
|
|
|
versionNumbers += versionNumber;
|
|
set.add(versionNumber);
|
|
}
|
|
}
|
|
|
|
assert(internals.guidSeparators.has(options.separator), 'separator must be one of true, false, "-", or ":"');
|
|
const separator = options.separator === undefined ? '[:-]?' :
|
|
options.separator === true ? '[:-]' :
|
|
options.separator === false ? '[]?' : `\\${options.separator}`;
|
|
|
|
let wrapperStart;
|
|
let wrapperEnd;
|
|
|
|
if (options.wrapper === undefined) {
|
|
wrapperStart = '[\\[{\\(]?';
|
|
wrapperEnd = '[\\]}\\)]?';
|
|
}
|
|
else if (options.wrapper === true) {
|
|
wrapperStart = '[\\[{\\(]';
|
|
wrapperEnd = '[\\]}\\)]';
|
|
}
|
|
else if (options.wrapper === false) {
|
|
wrapperStart = '';
|
|
wrapperEnd = '';
|
|
}
|
|
else {
|
|
wrapperStart = escapeRegex(options.wrapper);
|
|
wrapperEnd = escapeRegex(internals.guidBrackets[options.wrapper]);
|
|
}
|
|
|
|
const regex = new RegExp(
|
|
`^(${wrapperStart})[0-9A-F]{8}(${separator})[0-9A-F]{4}\\2?[${
|
|
versionNumbers || '0-9A-F'
|
|
}][0-9A-F]{3}\\2?[${
|
|
versionNumbers ? '89AB' : '0-9A-F'
|
|
}][0-9A-F]{3}\\2?[0-9A-F]{12}(${wrapperEnd})$`,
|
|
'i'
|
|
);
|
|
|
|
return this.$_addRule({ name: 'guid', args: { options }, regex });
|
|
},
|
|
|
|
validate(value, helpers, args, { regex }) {
|
|
|
|
const results = regex.exec(value);
|
|
|
|
if (!results) {
|
|
return helpers.error('string.guid');
|
|
}
|
|
|
|
const open = results[1];
|
|
const close = results[results.length - 1];
|
|
|
|
if ((open || close) && internals.guidBrackets[open] !== close) {
|
|
return helpers.error('string.guid');
|
|
}
|
|
|
|
return value;
|
|
}
|
|
},
|
|
|
|
hex: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['byteAligned', 'prefix']);
|
|
|
|
options = { byteAligned: false, prefix: false, ...options };
|
|
assert(typeof options.byteAligned === 'boolean', 'byteAligned must be boolean');
|
|
assert(typeof options.prefix === 'boolean' || options.prefix === 'optional', 'prefix must be boolean or "optional"');
|
|
|
|
return this.$_addRule({ name: 'hex', args: { options } });
|
|
},
|
|
validate(value, helpers, { options }) {
|
|
|
|
const re = options.prefix === 'optional' ?
|
|
internals.hexRegex.withOptionalPrefix :
|
|
options.prefix === true ?
|
|
internals.hexRegex.withPrefix :
|
|
internals.hexRegex.withoutPrefix;
|
|
if (!re.test(value)) {
|
|
return helpers.error('string.hex');
|
|
}
|
|
|
|
if (options.byteAligned &&
|
|
value.length % 2 !== 0) {
|
|
|
|
return helpers.error('string.hexAlign');
|
|
}
|
|
|
|
return value;
|
|
}
|
|
},
|
|
|
|
hostname: {
|
|
method() {
|
|
|
|
return this.$_addRule('hostname');
|
|
},
|
|
validate(value, helpers) {
|
|
|
|
if (isDomainValid(value, { minDomainSegments: 1 }) ||
|
|
internals.ipRegex.test(value)) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.hostname');
|
|
}
|
|
},
|
|
|
|
insensitive: {
|
|
method() {
|
|
|
|
return this.$_setFlag('insensitive', true);
|
|
}
|
|
},
|
|
|
|
ip: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['cidr', 'version']);
|
|
|
|
const { cidr, versions, regex } = ipRegex(options);
|
|
const version = options.version ? versions : undefined;
|
|
return this.$_addRule({ name: 'ip', args: { options: { cidr, version } }, regex });
|
|
},
|
|
validate(value, helpers, { options }, { regex }) {
|
|
|
|
if (regex.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
if (options.version) {
|
|
return helpers.error('string.ipVersion', { value, cidr: options.cidr, version: options.version });
|
|
}
|
|
|
|
return helpers.error('string.ip', { value, cidr: options.cidr });
|
|
}
|
|
},
|
|
|
|
isoDate: {
|
|
method() {
|
|
|
|
return this.$_addRule('isoDate');
|
|
},
|
|
validate(value, { error }) {
|
|
|
|
if (internals.isoDate(value)) {
|
|
return value;
|
|
}
|
|
|
|
return error('string.isoDate');
|
|
}
|
|
},
|
|
|
|
isoDuration: {
|
|
method() {
|
|
|
|
return this.$_addRule('isoDuration');
|
|
},
|
|
validate(value, helpers) {
|
|
|
|
if (internals.isoDurationRegex.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.isoDuration');
|
|
}
|
|
},
|
|
|
|
length: {
|
|
method(limit, encoding) {
|
|
|
|
return internals.length(this, 'length', limit, '=', encoding);
|
|
},
|
|
validate(value, helpers, { limit, encoding }, { name, operator, args }) {
|
|
|
|
const length = encoding ? Buffer && Buffer.byteLength(value, encoding) : value.length; // $lab:coverage:ignore$
|
|
if (Common.compare(length, limit, operator)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.' + name, { limit: args.limit, value, encoding });
|
|
},
|
|
args: [
|
|
{
|
|
name: 'limit',
|
|
ref: true,
|
|
assert: Common.limit,
|
|
message: 'must be a positive integer'
|
|
},
|
|
'encoding'
|
|
]
|
|
},
|
|
|
|
lowercase: {
|
|
method() {
|
|
|
|
return this.case('lower');
|
|
}
|
|
},
|
|
|
|
max: {
|
|
method(limit, encoding) {
|
|
|
|
return internals.length(this, 'max', limit, '<=', encoding);
|
|
},
|
|
args: ['limit', 'encoding']
|
|
},
|
|
|
|
min: {
|
|
method(limit, encoding) {
|
|
|
|
return internals.length(this, 'min', limit, '>=', encoding);
|
|
},
|
|
args: ['limit', 'encoding']
|
|
},
|
|
|
|
normalize: {
|
|
method(form = 'NFC') {
|
|
|
|
assert(internals.normalizationForms.includes(form), 'normalization form must be one of ' + internals.normalizationForms.join(', '));
|
|
|
|
return this.$_addRule({ name: 'normalize', args: { form } });
|
|
},
|
|
validate(value, { error }, { form }) {
|
|
|
|
if (value === value.normalize(form)) {
|
|
return value;
|
|
}
|
|
|
|
return error('string.normalize', { value, form });
|
|
},
|
|
convert: true
|
|
},
|
|
|
|
pattern: {
|
|
alias: 'regex',
|
|
method(regex, options = {}) {
|
|
|
|
assert(regex instanceof RegExp, 'regex must be a RegExp');
|
|
assert(!regex.flags.includes('g') && !regex.flags.includes('y'), 'regex should not use global or sticky mode');
|
|
|
|
if (typeof options === 'string') {
|
|
options = { name: options };
|
|
}
|
|
|
|
Common.assertOptions(options, ['invert', 'name']);
|
|
|
|
const errorCode = ['string.pattern', options.invert ? '.invert' : '', options.name ? '.name' : '.base'].join('');
|
|
return this.$_addRule({ name: 'pattern', args: { regex, options }, errorCode });
|
|
},
|
|
validate(value, helpers, { regex, options }, { errorCode }) {
|
|
|
|
const patternMatch = regex.test(value);
|
|
|
|
if (patternMatch ^ options.invert) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error(errorCode, { name: options.name, regex, value });
|
|
},
|
|
args: ['regex', 'options'],
|
|
multi: true
|
|
},
|
|
|
|
replace: {
|
|
method(pattern, replacement) {
|
|
|
|
if (typeof pattern === 'string') {
|
|
pattern = new RegExp(escapeRegex(pattern), 'g');
|
|
}
|
|
|
|
assert(pattern instanceof RegExp, 'pattern must be a RegExp');
|
|
assert(typeof replacement === 'string', 'replacement must be a String');
|
|
|
|
const obj = this.clone();
|
|
|
|
if (!obj.$_terms.replacements) {
|
|
obj.$_terms.replacements = [];
|
|
}
|
|
|
|
obj.$_terms.replacements.push({ pattern, replacement });
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
token: {
|
|
method() {
|
|
|
|
return this.$_addRule('token');
|
|
},
|
|
validate(value, helpers) {
|
|
|
|
if (/^\w+$/.test(value)) {
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.token');
|
|
}
|
|
},
|
|
|
|
trim: {
|
|
method(enabled = true) {
|
|
|
|
assert(typeof enabled === 'boolean', 'enabled must be a boolean');
|
|
|
|
return this.$_addRule({ name: 'trim', args: { enabled } });
|
|
},
|
|
validate(value, helpers, { enabled }) {
|
|
|
|
if (!enabled ||
|
|
value === value.trim()) {
|
|
|
|
return value;
|
|
}
|
|
|
|
return helpers.error('string.trim');
|
|
},
|
|
convert: true
|
|
},
|
|
|
|
truncate: {
|
|
method(enabled = true) {
|
|
|
|
assert(typeof enabled === 'boolean', 'enabled must be a boolean');
|
|
|
|
return this.$_setFlag('truncate', enabled);
|
|
}
|
|
},
|
|
|
|
uppercase: {
|
|
method() {
|
|
|
|
return this.case('upper');
|
|
}
|
|
},
|
|
|
|
uri: {
|
|
method(options = {}) {
|
|
|
|
Common.assertOptions(options, ['allowRelative', 'allowQuerySquareBrackets', 'domain', 'relativeOnly', 'scheme', 'encodeUri']);
|
|
|
|
if (options.domain) {
|
|
Common.assertOptions(options.domain, ['allowFullyQualified', 'allowUnicode', 'maxDomainSegments', 'minDomainSegments', 'tlds']);
|
|
}
|
|
|
|
const { regex, scheme } = uriRegex(options);
|
|
const domain = options.domain ? internals.addressOptions(options.domain) : null;
|
|
return this.$_addRule({ name: 'uri', args: { options }, regex, domain, scheme });
|
|
},
|
|
validate(value, helpers, { options }, { regex, domain, scheme }) {
|
|
|
|
if (['http:/', 'https:/'].includes(value)) { // scheme:/ is technically valid but makes no sense
|
|
return helpers.error('string.uri');
|
|
}
|
|
|
|
let match = regex.exec(value);
|
|
|
|
if (!match && helpers.prefs.convert && options.encodeUri) {
|
|
const encoded = encodeURI(value);
|
|
match = regex.exec(encoded);
|
|
if (match) {
|
|
value = encoded;
|
|
}
|
|
}
|
|
|
|
if (match) {
|
|
const matched = match[1] || match[2];
|
|
if (domain &&
|
|
(!options.allowRelative || matched) &&
|
|
!isDomainValid(matched, domain)) {
|
|
|
|
return helpers.error('string.domain', { value: matched });
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
if (options.relativeOnly) {
|
|
return helpers.error('string.uriRelativeOnly');
|
|
}
|
|
|
|
if (options.scheme) {
|
|
return helpers.error('string.uriCustomScheme', { scheme, value });
|
|
}
|
|
|
|
return helpers.error('string.uri');
|
|
}
|
|
}
|
|
},
|
|
|
|
manifest: {
|
|
|
|
build(obj, desc) {
|
|
|
|
if (desc.replacements) {
|
|
for (const { pattern, replacement } of desc.replacements) {
|
|
obj = obj.replace(pattern, replacement);
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
messages: {
|
|
'string.alphanum': '{{#label}} must only contain alpha-numeric characters',
|
|
'string.base': '{{#label}} must be a string',
|
|
'string.base64': '{{#label}} must be a valid base64 string',
|
|
'string.creditCard': '{{#label}} must be a credit card',
|
|
'string.dataUri': '{{#label}} must be a valid dataUri string',
|
|
'string.domain': '{{#label}} must contain a valid domain name',
|
|
'string.email': '{{#label}} must be a valid email',
|
|
'string.empty': '{{#label}} is not allowed to be empty',
|
|
'string.guid': '{{#label}} must be a valid GUID',
|
|
'string.hex': '{{#label}} must only contain hexadecimal characters',
|
|
'string.hexAlign': '{{#label}} hex decoded representation must be byte aligned',
|
|
'string.hostname': '{{#label}} must be a valid hostname',
|
|
'string.ip': '{{#label}} must be a valid ip address with a {{#cidr}} CIDR',
|
|
'string.ipVersion': '{{#label}} must be a valid ip address of one of the following versions {{#version}} with a {{#cidr}} CIDR',
|
|
'string.isoDate': '{{#label}} must be in iso format',
|
|
'string.isoDuration': '{{#label}} must be a valid ISO 8601 duration',
|
|
'string.length': '{{#label}} length must be {{#limit}} characters long',
|
|
'string.lowercase': '{{#label}} must only contain lowercase characters',
|
|
'string.max': '{{#label}} length must be less than or equal to {{#limit}} characters long',
|
|
'string.min': '{{#label}} length must be at least {{#limit}} characters long',
|
|
'string.normalize': '{{#label}} must be unicode normalized in the {{#form}} form',
|
|
'string.token': '{{#label}} must only contain alpha-numeric and underscore characters',
|
|
'string.pattern.base': '{{#label}} with value {:[.]} fails to match the required pattern: {{#regex}}',
|
|
'string.pattern.name': '{{#label}} with value {:[.]} fails to match the {{#name}} pattern',
|
|
'string.pattern.invert.base': '{{#label}} with value {:[.]} matches the inverted pattern: {{#regex}}',
|
|
'string.pattern.invert.name': '{{#label}} with value {:[.]} matches the inverted {{#name}} pattern',
|
|
'string.trim': '{{#label}} must not have leading or trailing whitespace',
|
|
'string.uri': '{{#label}} must be a valid uri',
|
|
'string.uriCustomScheme': '{{#label}} must be a valid uri with a scheme matching the {{#scheme}} pattern',
|
|
'string.uriRelativeOnly': '{{#label}} must be a valid relative uri',
|
|
'string.uppercase': '{{#label}} must only contain uppercase characters'
|
|
}
|
|
});
|
|
|
|
|
|
// Helpers
|
|
|
|
internals.addressOptions = function (options) {
|
|
|
|
if (!options) {
|
|
return internals.tlds || options; // $lab:coverage:ignore$
|
|
}
|
|
|
|
// minDomainSegments
|
|
|
|
assert(options.minDomainSegments === undefined ||
|
|
Number.isSafeInteger(options.minDomainSegments) && options.minDomainSegments > 0, 'minDomainSegments must be a positive integer');
|
|
|
|
// maxDomainSegments
|
|
|
|
assert(options.maxDomainSegments === undefined ||
|
|
Number.isSafeInteger(options.maxDomainSegments) && options.maxDomainSegments > 0, 'maxDomainSegments must be a positive integer');
|
|
|
|
// tlds
|
|
|
|
if (options.tlds === false) {
|
|
return options;
|
|
}
|
|
|
|
if (options.tlds === true ||
|
|
options.tlds === undefined) {
|
|
|
|
assert(internals.tlds, 'Built-in TLD list disabled');
|
|
return Object.assign({}, options, internals.tlds);
|
|
}
|
|
|
|
assert(typeof options.tlds === 'object', 'tlds must be true, false, or an object');
|
|
|
|
const deny = options.tlds.deny;
|
|
if (deny) {
|
|
if (Array.isArray(deny)) {
|
|
options = Object.assign({}, options, { tlds: { deny: new Set(deny) } });
|
|
}
|
|
|
|
assert(options.tlds.deny instanceof Set, 'tlds.deny must be an array, Set, or boolean');
|
|
assert(!options.tlds.allow, 'Cannot specify both tlds.allow and tlds.deny lists');
|
|
internals.validateTlds(options.tlds.deny, 'tlds.deny');
|
|
return options;
|
|
}
|
|
|
|
const allow = options.tlds.allow;
|
|
if (!allow) {
|
|
return { ...options, tlds: false };
|
|
}
|
|
|
|
if (allow === true) {
|
|
assert(internals.tlds, 'Built-in TLD list disabled');
|
|
return Object.assign({}, options, internals.tlds);
|
|
}
|
|
|
|
if (Array.isArray(allow)) {
|
|
options = Object.assign({}, options, { tlds: { allow: new Set(allow) } });
|
|
}
|
|
|
|
assert(options.tlds.allow instanceof Set, 'tlds.allow must be an array, Set, or boolean');
|
|
internals.validateTlds(options.tlds.allow, 'tlds.allow');
|
|
return options;
|
|
};
|
|
|
|
|
|
internals.validateTlds = function (set, source) {
|
|
|
|
for (const tld of set) {
|
|
assert(isDomainValid(tld, { minDomainSegments: 1, maxDomainSegments: 1 }), `${source} must contain valid top level domain names`);
|
|
}
|
|
};
|
|
|
|
|
|
internals.isoDate = function (value) {
|
|
|
|
if (!Common.isIsoDate(value)) {
|
|
return null;
|
|
}
|
|
|
|
if (/.*T.*[+-]\d\d$/.test(value)) { // Add missing trailing zeros to timeshift
|
|
value += '00';
|
|
}
|
|
|
|
const date = new Date(value);
|
|
if (isNaN(date.getTime())) {
|
|
return null;
|
|
}
|
|
|
|
return date.toISOString();
|
|
};
|
|
|
|
|
|
internals.length = function (schema, name, limit, operator, encoding) {
|
|
|
|
assert(!encoding || Buffer && Buffer.isEncoding(encoding), 'Invalid encoding:', encoding); // $lab:coverage:ignore$
|
|
|
|
return schema.$_addRule({ name, method: 'length', args: { limit, encoding }, operator });
|
|
};
|