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>
387 lines
10 KiB
Text
Executable file
387 lines
10 KiB
Text
Executable file
'use strict';
|
|
|
|
const { assert, merge } = require('@hapi/hoek');
|
|
|
|
const Any = require('./any');
|
|
const Common = require('../common');
|
|
const Compile = require('../compile');
|
|
const Errors = require('../errors');
|
|
const Ref = require('../ref');
|
|
|
|
|
|
const internals = {};
|
|
|
|
|
|
module.exports = Any.extend({
|
|
|
|
type: 'alternatives',
|
|
|
|
flags: {
|
|
|
|
match: { default: 'any' } // 'any', 'one', 'all'
|
|
},
|
|
|
|
terms: {
|
|
|
|
matches: { init: [], register: Ref.toSibling }
|
|
},
|
|
|
|
args(schema, ...schemas) {
|
|
|
|
if (schemas.length === 1) {
|
|
if (Array.isArray(schemas[0])) {
|
|
return schema.try(...schemas[0]);
|
|
}
|
|
}
|
|
|
|
return schema.try(...schemas);
|
|
},
|
|
|
|
validate(value, helpers) {
|
|
|
|
const { schema, error, state, prefs } = helpers;
|
|
|
|
// Match all or one
|
|
|
|
if (schema._flags.match) {
|
|
const matched = [];
|
|
const failed = [];
|
|
|
|
for (let i = 0; i < schema.$_terms.matches.length; ++i) {
|
|
const item = schema.$_terms.matches[i];
|
|
const localState = state.nest(item.schema, `match.${i}`);
|
|
localState.snapshot();
|
|
|
|
const result = item.schema.$_validate(value, localState, prefs);
|
|
if (!result.errors) {
|
|
matched.push(result.value);
|
|
localState.commit();
|
|
}
|
|
else {
|
|
failed.push(result.errors);
|
|
localState.restore();
|
|
}
|
|
}
|
|
|
|
if (matched.length === 0) {
|
|
const context = {
|
|
details: failed.map((f) => Errors.details(f, { override: false }))
|
|
};
|
|
|
|
return { errors: error('alternatives.any', context) };
|
|
}
|
|
|
|
// Match one
|
|
|
|
if (schema._flags.match === 'one') {
|
|
return matched.length === 1 ? { value: matched[0] } : { errors: error('alternatives.one') };
|
|
}
|
|
|
|
// Match all
|
|
|
|
if (matched.length !== schema.$_terms.matches.length) {
|
|
const context = {
|
|
details: failed.map((f) => Errors.details(f, { override: false }))
|
|
};
|
|
|
|
return { errors: error('alternatives.all', context) };
|
|
}
|
|
|
|
const isAnyObj = (alternative) => {
|
|
|
|
return alternative.$_terms.matches.some((v) => {
|
|
|
|
return v.schema.type === 'object' ||
|
|
(v.schema.type === 'alternatives' && isAnyObj(v.schema));
|
|
});
|
|
};
|
|
|
|
return isAnyObj(schema) ? { value: matched.reduce((acc, v) => merge(acc, v, { mergeArrays: false })) } : { value: matched[matched.length - 1] };
|
|
}
|
|
|
|
// Match any
|
|
|
|
const errors = [];
|
|
for (let i = 0; i < schema.$_terms.matches.length; ++i) {
|
|
const item = schema.$_terms.matches[i];
|
|
|
|
// Try
|
|
|
|
if (item.schema) {
|
|
const localState = state.nest(item.schema, `match.${i}`);
|
|
localState.snapshot();
|
|
|
|
const result = item.schema.$_validate(value, localState, prefs);
|
|
if (!result.errors) {
|
|
localState.commit();
|
|
return result;
|
|
}
|
|
|
|
localState.restore();
|
|
errors.push({ schema: item.schema, reports: result.errors });
|
|
continue;
|
|
}
|
|
|
|
// Conditional
|
|
|
|
const input = item.ref ? item.ref.resolve(value, state, prefs) : value;
|
|
const tests = item.is ? [item] : item.switch;
|
|
|
|
for (let j = 0; j < tests.length; ++j) {
|
|
const test = tests[j];
|
|
const { is, then, otherwise } = test;
|
|
|
|
const id = `match.${i}${item.switch ? '.' + j : ''}`;
|
|
if (!is.$_match(input, state.nest(is, `${id}.is`), prefs)) {
|
|
if (otherwise) {
|
|
return otherwise.$_validate(value, state.nest(otherwise, `${id}.otherwise`), prefs);
|
|
}
|
|
}
|
|
else if (then) {
|
|
return then.$_validate(value, state.nest(then, `${id}.then`), prefs);
|
|
}
|
|
}
|
|
}
|
|
|
|
return internals.errors(errors, helpers);
|
|
},
|
|
|
|
rules: {
|
|
|
|
conditional: {
|
|
method(condition, options) {
|
|
|
|
assert(!this._flags._endedSwitch, 'Unreachable condition');
|
|
assert(!this._flags.match, 'Cannot combine match mode', this._flags.match, 'with conditional rule');
|
|
assert(options.break === undefined, 'Cannot use break option with alternatives conditional');
|
|
|
|
const obj = this.clone();
|
|
|
|
const match = Compile.when(obj, condition, options);
|
|
const conditions = match.is ? [match] : match.switch;
|
|
for (const item of conditions) {
|
|
if (item.then &&
|
|
item.otherwise) {
|
|
|
|
obj.$_setFlag('_endedSwitch', true, { clone: false });
|
|
break;
|
|
}
|
|
}
|
|
|
|
obj.$_terms.matches.push(match);
|
|
return obj.$_mutateRebuild();
|
|
}
|
|
},
|
|
|
|
match: {
|
|
method(mode) {
|
|
|
|
assert(['any', 'one', 'all'].includes(mode), 'Invalid alternatives match mode', mode);
|
|
|
|
if (mode !== 'any') {
|
|
for (const match of this.$_terms.matches) {
|
|
assert(match.schema, 'Cannot combine match mode', mode, 'with conditional rules');
|
|
}
|
|
}
|
|
|
|
return this.$_setFlag('match', mode);
|
|
}
|
|
},
|
|
|
|
try: {
|
|
method(...schemas) {
|
|
|
|
assert(schemas.length, 'Missing alternative schemas');
|
|
Common.verifyFlat(schemas, 'try');
|
|
|
|
assert(!this._flags._endedSwitch, 'Unreachable condition');
|
|
|
|
const obj = this.clone();
|
|
for (const schema of schemas) {
|
|
obj.$_terms.matches.push({ schema: obj.$_compile(schema) });
|
|
}
|
|
|
|
return obj.$_mutateRebuild();
|
|
}
|
|
}
|
|
},
|
|
|
|
overrides: {
|
|
|
|
label(name) {
|
|
|
|
const obj = this.$_parent('label', name);
|
|
const each = (item, source) => {
|
|
|
|
return source.path[0] !== 'is' && typeof item._flags.label !== 'string' ? item.label(name) : undefined;
|
|
};
|
|
|
|
return obj.$_modify({ each, ref: false });
|
|
},
|
|
|
|
isAsync() {
|
|
|
|
if (this.$_terms.externals?.length) {
|
|
return true;
|
|
}
|
|
|
|
for (const match of this.$_terms.matches) {
|
|
|
|
if (match.schema?.isAsync()) {
|
|
return true;
|
|
}
|
|
|
|
if (match.then?.isAsync()) {
|
|
return true;
|
|
}
|
|
|
|
if (match.otherwise?.isAsync()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
},
|
|
|
|
rebuild(schema) {
|
|
|
|
// Flag when an alternative type is an array
|
|
|
|
const each = (item) => {
|
|
|
|
if (Common.isSchema(item) &&
|
|
item.type === 'array') {
|
|
|
|
schema.$_setFlag('_arrayItems', true, { clone: false });
|
|
}
|
|
};
|
|
|
|
schema.$_modify({ each });
|
|
},
|
|
|
|
manifest: {
|
|
|
|
build(obj, desc) {
|
|
|
|
if (desc.matches) {
|
|
for (const match of desc.matches) {
|
|
const { schema, ref, is, not, then, otherwise } = match;
|
|
if (schema) {
|
|
obj = obj.try(schema);
|
|
}
|
|
else if (ref) {
|
|
obj = obj.conditional(ref, { is, then, not, otherwise, switch: match.switch });
|
|
}
|
|
else {
|
|
obj = obj.conditional(is, { then, otherwise });
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
},
|
|
|
|
messages: {
|
|
'alternatives.all': '{{#label}} does not match all of the required types',
|
|
'alternatives.any': '{{#label}} does not match any of the allowed types',
|
|
'alternatives.match': '{{#label}} does not match any of the allowed types',
|
|
'alternatives.one': '{{#label}} matches more than one allowed type',
|
|
'alternatives.types': '{{#label}} must be one of {{#types}}'
|
|
}
|
|
});
|
|
|
|
|
|
// Helpers
|
|
|
|
internals.errors = function (failures, { error, state }) {
|
|
|
|
// Nothing matched due to type criteria rules
|
|
|
|
if (!failures.length) {
|
|
return { errors: error('alternatives.any') };
|
|
}
|
|
|
|
// Single error
|
|
|
|
if (failures.length === 1) {
|
|
return { errors: failures[0].reports };
|
|
}
|
|
|
|
// Analyze reasons
|
|
|
|
const valids = new Set();
|
|
const complex = [];
|
|
|
|
for (const { reports, schema } of failures) {
|
|
|
|
// Multiple errors (!abortEarly)
|
|
|
|
if (reports.length > 1) {
|
|
return internals.unmatched(failures, error);
|
|
}
|
|
|
|
// Custom error
|
|
|
|
const report = reports[0];
|
|
if (report instanceof Errors.Report === false) {
|
|
return internals.unmatched(failures, error);
|
|
}
|
|
|
|
// Internal object or array error
|
|
|
|
if (report.state.path.length !== state.path.length) {
|
|
complex.push({ type: schema.type, report });
|
|
continue;
|
|
}
|
|
|
|
// Valids
|
|
|
|
if (report.code === 'any.only') {
|
|
for (const valid of report.local.valids) {
|
|
valids.add(valid);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Base type
|
|
|
|
const [type, code] = report.code.split('.');
|
|
if (code !== 'base') {
|
|
complex.push({ type: schema.type, report });
|
|
}
|
|
else if (report.code === 'object.base') {
|
|
valids.add(report.local.type);
|
|
}
|
|
else {
|
|
valids.add(type);
|
|
}
|
|
}
|
|
|
|
// All errors are base types or valids
|
|
|
|
if (!complex.length) {
|
|
return { errors: error('alternatives.types', { types: [...valids] }) };
|
|
}
|
|
|
|
// Single complex error
|
|
|
|
if (complex.length === 1) {
|
|
return { errors: complex[0].report };
|
|
}
|
|
|
|
return internals.unmatched(failures, error);
|
|
};
|
|
|
|
|
|
internals.unmatched = function (failures, error) {
|
|
|
|
const errors = [];
|
|
for (const failure of failures) {
|
|
errors.push(...failure.reports);
|
|
}
|
|
|
|
return { errors: error('alternatives.match', Errors.details(errors, { override: false })) };
|
|
};
|