makeDescriptor (prop, schema, opts) {
    const descriptor = {
      // Better to allow configurability, but at the user's own risk
      configurable: true,
      // These properties are enumerable by default, but regardless of their
      // enumerability, they won't be "own" properties of individual records
      enumerable: schema.enumerable === undefined ? true : !!schema.enumerable
    }
    // Cache a few strings for optimal performance
    const keyPath = `props.${prop}`
    const previousPath = `previous.${prop}`
    const getter = opts.getter
    const setter = opts.setter
    const unsetter = opts.unsetter
    const track = utils.isBoolean(opts.track) ? opts.track : schema.track

    descriptor.get = function () {
      return this._get(keyPath)
    }

    if (utils.isFunction(schema.get)) {
      const originalGet = descriptor.get
      descriptor.get = function () {
        return schema.get.call(this, originalGet)
      }
    }

    descriptor.set = function (value) {
      // These are accessed a lot
      const _get = this[getter]
      const _set = this[setter]
      const _unset = this[unsetter]
      // Optionally check that the new value passes validation
      if (!_get(noValidatePath)) {
        const errors = schema.validate(value, { path: [prop] })
        if (errors) {
          // Immediately throw an error, preventing the record from getting into
          // an invalid state
          const error = new Error(validationFailureMsg)
          error.errors = errors
          throw error
        }
      }
      // TODO: Make it so tracking can be turned on for all properties instead of
      // only per-property
      if (track && !_get(creatingPath)) {
        // previous is versioned on database commit
        // props are versioned on set()
        const previous = _get(previousPath)
        const current = _get(keyPath)
        let changing = _get(changingPath)
        let changed = _get(changedPath)

        if (!changing) {
          // Track properties that are changing in the current event loop
          changed = []
        }

        // Add changing properties to this array once at most
        const index = changed.indexOf(prop)
        if (current !== value && index === -1) {
          changed.push(prop)
        }
        if (previous === value) {
          if (index >= 0) {
            changed.splice(index, 1)
          }
        }
        // No changes in current event loop
        if (!changed.length) {
          changing = false
          _unset(changingPath)
          _unset(changedPath)
          // Cancel pending change event
          if (_get(eventIdPath)) {
            clearTimeout(_get(eventIdPath))
            _unset(eventIdPath)
          }
        }
        // Changes detected in current event loop
        if (!changing && changed.length) {
          _set(changedPath, changed)
          _set(changingPath, true)
          // Saving the timeout id allows us to batch all changes in the same
          // event loop into a single "change"
          // TODO: Optimize
          _set(eventIdPath, setTimeout(() => {
            // Previous event loop where changes were gathered has ended, so
            // notify any listeners of those changes and prepare for any new
            // changes
            _unset(changedPath)
            _unset(eventIdPath)
            _unset(changingPath)
            // TODO: Optimize
            if (!_get(silentPath)) {
              let i
              for (i = 0; i < changed.length; i++) {
                this.emit('change:' + changed[i], this, utils.get(this, changed[i]))
              }

              const changes = utils.diffObjects({ [prop]: value }, { [prop]: current })

              if (_get(keepChangeHistoryPath)) {
                const changeRecord = utils.plainCopy(changes)
                changeRecord.timestamp = new Date().getTime()
                let changeHistory = _get(changeHistoryPath)
                !changeHistory && _set(changeHistoryPath, (changeHistory = []))
                changeHistory.push(changeRecord)
              }
              this.emit('change', this, changes)
            }
            _unset(silentPath)
          }, 0))
        }
      }
      _set(keyPath, value)
      return value
    }

    if (utils.isFunction(schema.set)) {
      const originalSet = descriptor.set
      descriptor.set = function (value) {
        return schema.set.call(this, value, originalSet)
      }
    }

    return descriptor
  }
Assemble a property descriptor for tracking and validating changes to
a property according to the given schema. This method is called when
{@link Mapper#applySchema} is set to `true`.

@name Schema#makeDescriptor
@method
@param {string} prop The property name.
@param {(Schema|object)} schema The schema for the property.
@param {object} [opts] Optional configuration.
@param {function} [opts.getter] Custom getter function.
@param {function} [opts.setter] Custom setter function.
@param {function} [opts.track] Whether to track changes.
@returns {object} A property descriptor for the given schema.