function defineKey({ prop, subprops, prototype, prefix, options }) {
  Document = Document || require('../../document');
  const path = (prefix ? prefix + '.' : '') + prop;
  prefix = prefix || '';
  const useGetOptions = prefix ? Object.freeze({}) : noDottedPathGetOptions;

  if (subprops) {
    Object.defineProperty(prototype, prop, {
      enumerable: true,
      configurable: true,
      get: function() {
        const _this = this;
        if (!this.$__.getters) {
          this.$__.getters = {};
        }

        if (!this.$__.getters[path]) {
          const nested = Object.create(Document.prototype, getOwnPropertyDescriptors(this));

          // save scope for nested getters/setters
          if (!prefix) {
            nested.$__[scopeSymbol] = this;
          }
          nested.$__.nestedPath = path;

          Object.defineProperty(nested, 'schema', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: prototype.schema
          });

          Object.defineProperty(nested, '$__schema', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: prototype.schema
          });

          Object.defineProperty(nested, documentSchemaSymbol, {
            enumerable: false,
            configurable: true,
            writable: false,
            value: prototype.schema
          });

          Object.defineProperty(nested, 'toObject', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: function() {
              return clone(_this.get(path, null, {
                virtuals: this &&
                  this.schema &&
                  this.schema.options &&
                  this.schema.options.toObject &&
                  this.schema.options.toObject.virtuals || null
              }));
            }
          });

          Object.defineProperty(nested, '$__get', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: function() {
              return _this.get(path, null, {
                virtuals: this && this.schema && this.schema.options && this.schema.options.toObject && this.schema.options.toObject.virtuals || null
              });
            }
          });

          Object.defineProperty(nested, 'toJSON', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: function() {
              return _this.get(path, null, {
                virtuals: this && this.schema && this.schema.options && this.schema.options.toJSON && this.schema.options.toJSON.virtuals || null
              });
            }
          });

          Object.defineProperty(nested, '$__isNested', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: true
          });

          Object.defineProperty(nested, '$isEmpty', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: function() {
              return Object.keys(this.get(path, null, _isEmptyOptions) || {}).length === 0;
            }
          });

          Object.defineProperty(nested, '$__parent', {
            enumerable: false,
            configurable: true,
            writable: false,
            value: this
          });

          compile(subprops, nested, path, options);
          this.$__.getters[path] = nested;
        }

        return this.$__.getters[path];
      },
      set: function(v) {
        if (v != null && v.$__isNested) {
          // Convert top-level to POJO, but leave subdocs hydrated so `$set`
          // can handle them. See gh-9293.
          v = v.$__get();
        } else if (v instanceof Document && !v.$__isNested) {
          v = v.$toObject(internalToObjectOptions);
        }
        const doc = this.$__[scopeSymbol] || this;
        doc.$set(path, v);
      }
    });
  } else {
    Object.defineProperty(prototype, prop, {
      enumerable: true,
      configurable: true,
      get: function() {
        return this[getSymbol].call(
          this.$__[scopeSymbol] || this,
          path,
          null,
          useGetOptions
        );
      },
      set: function(v) {
        this.$set.call(this.$__[scopeSymbol] || this, path, v);
      }
    });
  }
}
Defines the accessor named prop on the incoming prototype.
@param {Object} options
@param {String} options.prop
@param {Boolean} options.subprops
@param {Any} options.prototype
@param {String} [options.prefix]
@param {Object} options.options
@api private