UNPKG

@knorm/knorm

Version:

A JavaScript ORM written using ES6 classes

538 lines (537 loc) 19.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const validator_1 = require("validator"); const Model_1 = require("./Model"); const ValidationError_1 = require("./ValidationError"); const isSet = (value) => value !== undefined && value !== null; /** * Creates and holds configuration for fields, e.g. how to validate or cast * fields. */ class Field { /** * Creates a {@link Field} instance. * * @param {object} [config] The field's configuration. */ constructor(config = {}) { const { name, model, type, validate, cast, column, primary, updated = true, unique, methods, } = config; if (!name) { throw new Error('Field requires a name'); } const path = config.path || name; if (!model || !(model.prototype instanceof Model_1.Model)) { throw new Error(`Field \`${path}\` requires a subclass of \`Model\``); } const field = `\`${model.name}.${path}\``; if (!type) { throw new Error(`Field ${field} has no type configured`); } if (!this.constructor.types.includes(type)) { throw new Error(`Field ${field} has an invalid type \`${type}\``); } if (validate && typeof validate !== 'function') { throw new Error(`\`validate\` option for field ${field} should be a function`); } if (cast) { if (cast.forSave) { if (typeof cast.forSave !== 'function') { throw new Error(`\`cast.forSave\` option for field ${field} should be a function`); } this.castors = this.castors || {}; this.castors.forSave = cast.forSave; } if (cast.forFetch) { if (typeof cast.forFetch !== 'function') { throw new Error(`\`cast.forFetch\` option for field ${field} should be a function`); } this.castors = this.castors || {}; this.castors.forFetch = cast.forFetch; } } this.config = config; this.name = name; this.type = type; this.path = path; this.model = model; this.updated = updated; this.column = column || this.getColumnName(name); if (primary) { this.primary = true; } if (unique) { this.unique = true; } if (methods) { this.methods = true; } if (config.default !== undefined) { this.default = config.default; } this.validators = this._createValidators(config); } _createValidators(config) { const { type, validate, required, minLength, maxLength, oneOf, equals, regex, shape, } = config; const validators = { type }; if (validate) { validators.validate = validate; } if (required) { validators.required = required; } if (minLength) { validators.minLength = minLength; } if (maxLength !== undefined) { validators.maxLength = maxLength; } if (oneOf) { validators.oneOf = oneOf; } if (equals !== undefined) { validators.equals = equals; } if (regex) { validators.regex = regex; } if (shape) { validators.shape = this._createShapeValidators(shape); } return validators; } // TODO: write regression tests for `new Field` vs `new this.constructor` _createShapeValidators(shape) { if (typeof shape === 'string') { shape = { type: shape }; } if (shape.type && typeof shape.type === 'string') { // is item shape return new this.constructor(Object.assign({}, shape, { name: this.name, path: this.path, model: this.model, })); } return Object.keys(shape).reduce((validators, key) => { let name; let path; let config = shape[key]; if (typeof config === 'string') { config = { type: config }; } if (typeof config === 'object') { name = key; path = `${this.path}.${name}`; } else { // if config is not an object, then it's invalid. the Field constructor // will therefore throw an error; with the next line, we just ensure it // throws the right error name = this.name; } validators[name] = new this.constructor(Object.assign({}, config, { name, path, model: this.model, })); return validators; }, {}); } getColumnName(fieldName) { return fieldName; } throwValidationError(value, validator) { throw new this.constructor.ValidationError({ value, validator, field: this, }); } validateIsRequired(value) { if (!isSet(value)) { this.throwValidationError(value, { required: true }); } } _validateTypeWith(value, type, validate) { if (!isSet(value)) { return true; } if (!validate(value)) { this.throwValidationError(value, { type }); } } _validateIsAny(value, type) { this._validateTypeWith(value, type, () => true); } validateIsAny(value, type) { this._validateIsAny(value, type); } validateIsNumber(value, type) { this._validateTypeWith(value, type, (value) => typeof value === 'number'); } _validateIsString(value, type) { this._validateTypeWith(value, type, (value) => typeof value === 'string'); } validateIsString(value, type) { this._validateIsString(value, type); } validateIsText(value, type) { this._validateIsString(value, type); } validateIsEmail(value, type) { this._validateIsString(value, 'string'); this._validateTypeWith(value, type, (value) => validator_1.isEmail(value)); } validateIsBinary(value, type) { this._validateTypeWith(value, type, (value) => value instanceof Buffer); } validateIsInteger(value, type) { this._validateTypeWith(value, type, (value) => Number.isInteger(value)); } validateIsBoolean(value, type) { this._validateTypeWith(value, type, (value) => typeof value === 'boolean'); } _validateIsDate(value, type) { this._validateTypeWith(value, type, (value) => value instanceof Date); } validateIsDate(value, type) { this._validateIsDate(value, type); } validateIsDateTime(value, type) { this._validateIsDate(value, type); } validateIsUuid(value, type) { this._validateTypeWith(value, type, (value) => validator_1.isUUID(value)); } validateIsUuidV4(value, type) { this._validateTypeWith(value, type, (value) => validator_1.isUUID(value, 4)); } validateIsDecimal(value, type) { // validator.isDecimal requires the value to be a string this._validateTypeWith(value, type, (value) => validator_1.isDecimal(String(value))); } validateIsJson(value, type) { this._validateIsAny(value, type); } validateIsJsonB(value, type) { this._validateIsAny(value, type); } validateIsObject(value, type) { this._validateTypeWith(value, type, (value) => typeof value === 'object'); } validateIsArray(value, type) { this._validateTypeWith(value, type, (value) => Array.isArray(value)); } validateTypeIs(value, type) { switch (type) { case 'any': return this.validateIsAny(value, type); case 'number': return this.validateIsNumber(value, type); case 'string': return this.validateIsString(value, type); case 'text': return this.validateIsText(value, type); case 'binary': return this.validateIsBinary(value, type); case 'email': return this.validateIsEmail(value, type); case 'integer': return this.validateIsInteger(value, type); case 'boolean': return this.validateIsBoolean(value, type); case 'date': return this.validateIsDate(value, type); case 'dateTime': return this.validateIsDateTime(value, type); case 'uuid': return this.validateIsUuid(value, type); case 'uuid4': return this.validateIsUuidV4(value, type); case 'json': return this.validateIsJson(value, type); case 'jsonb': return this.validateIsJsonB(value, type); case 'object': return this.validateIsObject(value, type); case 'array': return this.validateIsArray(value, type); case 'decimal': return this.validateIsDecimal(value, type); default: throw new Error(`no validator has been added for \`${type}\` types`); } } validateMinLengthIs(value, minLength) { if (!isSet(value)) { return true; } if (value.length < minLength) { this.throwValidationError(value, { minLength }); } } validateMaxLengthIs(value, maxLength) { if (!isSet(value)) { return true; } if (value.length > maxLength) { this.throwValidationError(value, { maxLength }); } } validateIsOneOf(value, oneOf) { if (!isSet(value)) { return true; } if (!oneOf.includes(value)) { this.throwValidationError(value, { oneOf }); } } // TODO: support `equals` validator for json fields validateEquals(value, equals) { if (!isSet(value)) { return true; } if (value !== equals) { this.throwValidationError(value, { equals }); } } _validateRegexMatching(value, regex) { if (!regex.test(value)) { this.throwValidationError(value, { regex, matching: true }); } } _validateRegexNotMatching(value, regex) { if (regex.test(value)) { this.throwValidationError(value, { regex, notMatching: true }); } } validateWithRegex(value, regex) { if (!isSet(value)) { return true; } if (regex instanceof RegExp) { return this._validateRegexMatching(value, regex); } const { matching, notMatching } = regex; if (matching) { this._validateRegexMatching(value, matching); } if (notMatching) { this._validateRegexNotMatching(value, notMatching); } } /** * Custom validator function, note that `async` validator functions, or * functions that return a {@link Promise}, are supported. * * Validation for the value will be failed if the function: * - throws an error * - returns `false` * - returns a `Promise` that is rejected with an error * - returns a `Promise` that is resolved with `false` * * This function may also return an object with the regular * [validators](/guides/fields.md#field-config), or resolving the `Promise` * with an object with validators, including another custom validator * function! * * @callback Field~customValidator * @param {any} value The value to validate. * @param {Model} model The {@link Model} instance where the field value is * set. * * @returns {Promise|boolean|object} */ /** * Validates a value with a custom validator function. * * @param {any} value The value to validate * @param {Field~customValidator} validate The validator function. * @param {Model} model The {@link Model} instance where the field's value is * set. * * @returns {Promise} A `Promise` that is resolved if the `value` param passes * custom validation, or otherwise rejected. */ validateWithCustom(value, validate, model) { return __awaiter(this, void 0, void 0, function* () { if (value === undefined) { return true; } const returnValue = yield validate(value, model); if (returnValue === false) { this.throwValidationError(value, { validate }); } if (typeof returnValue === 'object') { const validators = this._createValidators(returnValue); return this.validateWithValidators(value, validators, model); } }); } validateWithShape(value, shape, modelInstance) { return __awaiter(this, void 0, void 0, function* () { const isRootShape = shape instanceof Field; if (typeof value === 'string' && !(isRootShape && shape.type === 'string')) { try { value = JSON.parse(value); } catch (e) { this.throwValidationError(value, { type: this.type }); } } if (isRootShape) { if (this.type === 'array') { if (!value || !value.length) { if (shape.validators.required) { shape.valueIndex = undefined; shape.throwValidationError(value, { required: true }); } else { return true; } } return Promise.all(value.map((item, index) => __awaiter(this, void 0, void 0, function* () { shape.valueIndex = index; return shape.validate(item, modelInstance); }))); } else { return shape.validate(value, modelInstance); } } if (!isSet(value)) { return true; } return Promise.all(Object.values(shape).map((field) => __awaiter(this, void 0, void 0, function* () { return field.validate(value[field.name], modelInstance); }))); }); } validateWithValidators(value, validators, modelInstance) { return __awaiter(this, void 0, void 0, function* () { const { required, type, minLength, maxLength, oneOf, equals, regex, validate, shape, } = validators; if (required) { this.validateIsRequired(value); } if (!(value instanceof this.knorm.Query.prototype.sql)) { if (type) { this.validateTypeIs(value, type); } if (minLength) { this.validateMinLengthIs(value, minLength); } if (maxLength !== undefined) { this.validateMaxLengthIs(value, maxLength); } if (oneOf) { this.validateIsOneOf(value, oneOf); } if (equals !== undefined) { this.validateEquals(value, equals); } if (regex) { this.validateWithRegex(value, regex); } if (shape) { yield this.validateWithShape(value, shape, modelInstance); } } if (validate) { yield this.validateWithCustom(value, validate, modelInstance); } }); } validate(value, modelInstance) { return __awaiter(this, void 0, void 0, function* () { yield this.validateWithValidators(value, this.validators, modelInstance); }); } /** * Casts a {@link Field}'s value with its configured cast functions. * * @param {any} value The value to be cast. * @param {Model} model The {@link Model} instance where the field's * value is set. The cast functions will be called with this instance as a * parameter. * @param {object} options Cast options. * @param {boolean} options.forSave Whether or not to cast for save operations * (i.e before insert or update). This function is called with the `value` as * the first paramter and `model` as the second parameter. * @param {boolean} options.forFetch Whether or not to cast for fetch * operations (i.e after fetch or any other operations that return data from * the database). This function is called with the `value` as the first * paramter and `model` as the second parameter. * * @returns {any} The cast value, or `undefined` if no cast functions are * configured for the {@link Field} instance. */ cast(value, model, { forSave, forFetch }) { if (!this.castors) { return; } if (forSave) { if (!this.castors.forSave) { return; } return this.castors.forSave(value, model); } if (forFetch) { if (!this.castors.forFetch) { return; } return this.castors.forFetch(value, model); } } /** * Returns the default value for a field from the {@link Field}'s * configuration. * * @param {Model} model The {@link Model} instance where the field's default * value will be set. If the default is configured as a function, the function * is called with `model` as the first parameter. * * @returns {any|undefined} The default value or `undefined` if there's no * default value set for the field. */ getDefault(model) { if (typeof this.default === 'function') { return this.default(model); } return this.default; } } exports.Field = Field; Field.knorm = Field.prototype.knorm = null; // TODO: add password type // TODO: add all the types supported by knex // TODO: document how to share validation logic with existing types // TODO: document how to add new types Field.types = [ 'text', 'json', 'jsonb', 'uuid', 'uuid4', 'binary', 'decimal', 'string', 'boolean', 'integer', 'dateTime', 'date', // custom types: 'email', 'any', 'number', 'object', 'array', ]; Field.ValidationError = ValidationError_1.ValidationError;