Skip to content

Unexpected Element generated (__cachedRelations included in payload message) #77

@liudonghua123

Description

@liudonghua123

Bug or feature request

  • Bug
  • Feature request

Description of feature (or steps to reproduce if bug)

I have a existed web service. and I followed the tutorial Connecting to SOAP web services. And When I invoke a simple operation. It gave me the unexpected element exception. I looked up the documentation and could not find a option to disable generating these internal properties.

I use Wireshark to capture the related packets

POST /apiws/services/API HTTP/1.1
User-Agent: loopback-connector-soap/3.0.1
Accept: text/html,application/xhtml+xml,application/xml,text/xml;q=0.9,*/*;q=0.8
Accept-Encoding: none
Accept-Charset: utf-8
Connection: close
Host: mail.ynu.edu.cn:9900
Content-Length: 17821
Content-Type: text/xml; charset=utf-8
SOAPAction: ""

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header/>
  <soap:Body>
    <ns1:authenticate xmlns:ns1="http://coremail.cn/apiws">
      <__cachedRelations/>
      <__data>
        <user_at_domain>my_mail_address</user_at_domain>
        <password>my_mail_password</password>
      </__data>
      <__dataSource/>
      <__strict>false</__strict>
      <__persisted>false</__persisted>
      <user_at_domain>my_mail_address</user_at_domain>
      <password>my_mail_password</password>
      <_initProperties>function (data, options) {
  var self = this;
  var ctor = this.constructor;

  if (typeof data !== 'undefined' &amp;&amp; data.constructor &amp;&amp;
      typeof (data.constructor) !== 'function') {
    throw new Error(g.f('Property name "{{constructor}}" is not allowed in %s data', ctor.modelName));
  }

  if (data instanceof ctor) {
    // Convert the data to be plain object to avoid pollutions
    data = data.toObject(false);
  }
  var properties = _extend({}, ctor.definition.properties);
  data = data || {};

  if (typeof ctor.applyProperties === 'function') {
    ctor.applyProperties(data);
  }

  options = options || {};
  var applySetters = options.applySetters;
  var applyDefaultValues = options.applyDefaultValues;
  var strict = options.strict;

  if (strict === undefined) {
    strict = ctor.definition.settings.strict;
  } else if (strict === 'throw') {
    g.warn('Warning: Model %s, {{strict mode: `throw`}} has been removed, ' +
      'please use {{`strict: true`}} instead, which returns' +
      '{{`Validation Error`}} for unknown properties,', ctor.modelName);
  }

  var persistUndefinedAsNull = ctor.definition.settings.persistUndefinedAsNull;

  if (ctor.hideInternalProperties) {
    // Object.defineProperty() is expensive. We only try to make the internal
    // properties hidden (non-enumerable) if the model class has the
    // `hideInternalProperties` set to true
    Object.defineProperties(this, {
      __cachedRelations: {
        writable: true,
        enumerable: false,
        configurable: true,
        value: {},
      },

      __data: {
        writable: true,
        enumerable: false,
        configurable: true,
        value: {},
      },

      // Instance level data source
      __dataSource: {
        writable: true,
        enumerable: false,
        configurable: true,
        value: options.dataSource,
      },

      // Instance level strict mode
      __strict: {
        writable: true,
        enumerable: false,
        configurable: true,
        value: strict,
      },

      __persisted: {
        writable: true,
        enumerable: false,
        configurable: true,
        value: false,
      },
    });

    if (strict) {
      Object.defineProperty(this, '__unknownProperties', {
        writable: true,
        enumerable: false,
        configrable: true,
        value: [],
      });
    }
  } else {
    this.__cachedRelations = {};
    this.__data = {};
    this.__dataSource = options.dataSource;
    this.__strict = strict;
    this.__persisted = false;
    if (strict) {
      this.__unknownProperties = [];
    }
  }

  if (options.persisted !== undefined) {
    this.__persisted = options.persisted === true;
  }

  if (data.__cachedRelations) {
    this.__cachedRelations = data.__cachedRelations;
  }

  var keys = Object.keys(data);

  if (Array.isArray(options.fields)) {
    keys = keys.filter(function(k) {
      return (options.fields.indexOf(k) != -1);
    });
  }

  var size = keys.length;
  var p, propVal;
  for (var k = 0; k &lt; size; k++) {
    p = keys[k];
    propVal = data[p];
    if (typeof propVal === 'function') {
      continue;
    }

    if (propVal === undefined &amp;&amp; persistUndefinedAsNull) {
      propVal = null;
    }

    if (properties[p]) {
      // Managed property
      if (applySetters || properties[p].id) {
        self[p] = propVal;
      } else {
        self.__data[p] = propVal;
      }
    } else if (ctor.relations[p]) {
      var relationType = ctor.relations[p].type;

      var modelTo;
      if (!properties[p]) {
        modelTo = ctor.relations[p].modelTo || ModelBaseClass;
        var multiple = ctor.relations[p].multiple;
        var typeName = multiple ? 'Array' : modelTo.modelName;
        var propType = multiple ? [modelTo] : modelTo;
        properties[p] = {name: typeName, type: propType};
        /* Issue #1252
        this.setStrict(false);
        */
      }

      // Relation
      if (relationType === 'belongsTo' &amp;&amp; propVal != null) {
        // If the related model is populated
        self.__data[ctor.relations[p].keyFrom] = propVal[ctor.relations[p].keyTo];

        if (ctor.relations[p].options.embedsProperties) {
          var fields = fieldsToArray(ctor.relations[p].properties,
            modelTo.definition.properties, modelTo.settings.strict);
          if (!~fields.indexOf(ctor.relations[p].keyTo)) {
            fields.push(ctor.relations[p].keyTo);
          }
          self.__data[p] = new modelTo(propVal, {
            fields: fields,
            applySetters: false,
            persisted: options.persisted,
          });
        }
      }

      self.__cachedRelations[p] = propVal;
    } else {
      // Un-managed property
      if (strict === false || self.__cachedRelations[p]) {
        self[p] = self.__data[p] =
          (propVal !== undefined) ? propVal : self.__cachedRelations[p];

        // Throw error for properties with unsupported names
        if (/\./.test(p)) {
          throw new Error(g.f(
            'Property names containing dot(s) are not supported. ' +
            'Model: %s, dynamic property: %s',
            this.constructor.modelName, p
          ));
        }
      } else {
        if (strict !== 'filter') {
          this.__unknownProperties.push(p);
        }
      }
    }
  }

  keys = Object.keys(properties);

  if (Array.isArray(options.fields)) {
    keys = keys.filter(function(k) {
      return (options.fields.indexOf(k) != -1);
    });
  }

  size = keys.length;

  for (k = 0; k &lt; size; k++) {
    p = keys[k];
    propVal = self.__data[p];
    var type = properties[p].type;

    // Set default values
    if (applyDefaultValues &amp;&amp; propVal === undefined) {
      var def = properties[p]['default'];
      if (def !== undefined) {
        if (typeof def === 'function') {
          if (def === Date) {
            // FIXME: We should coerce the value in general
            // This is a work around to {default: Date}
            // Date() will return a string instead of Date
            def = new Date();
          } else {
            def = def();
          }
        } else if (type.name === 'Date' &amp;&amp; def === '$now') {
          def = new Date();
        }
        // FIXME: We should coerce the value
        // will implement it after we refactor the PropertyDefinition
        self.__data[p] = propVal = def;
      }
    }

    // Set default value using a named function
    if (applyDefaultValues &amp;&amp; propVal === undefined) {
      var defn = properties[p].defaultFn;
      switch (defn) {
        case undefined:
          break;
        case 'guid':
        case 'uuid':
          // Generate a v1 (time-based) id
          propVal = uuid.v1();
          break;
        case 'uuidv4':
          // Generate a RFC4122 v4 UUID
          propVal = uuid.v4();
          break;
        case 'now':
          propVal = new Date();
          break;
        case 'shortid':
          propVal = shortid.generate();
          break;
        default:
          // TODO Support user-provided functions via a registry of functions
          g.warn('Unknown default value provider %s', defn);
      }
      // FIXME: We should coerce the value
      // will implement it after we refactor the PropertyDefinition
      if (propVal !== undefined)
        self.__data[p] = propVal;
    }

    if (propVal === undefined &amp;&amp; persistUndefinedAsNull) {
      self.__data[p] = propVal = null;
    }

    // Handle complex types (JSON/Object)
    if (!BASE_TYPES[type.name]) {
      if (typeof self.__data[p] !== 'object' &amp;&amp; self.__data[p]) {
        try {
          self.__data[p] = JSON.parse(self.__data[p] + '');
        } catch (e) {
          self.__data[p] = String(self.__data[p]);
        }
      }

      if (type.prototype instanceof ModelBaseClass) {
        if (!(self.__data[p] instanceof type) &amp;&amp;
            typeof self.__data[p] === 'object' &amp;&amp;
            self.__data[p] !== null) {
          self.__data[p] = new type(self.__data[p]);
        }
      } else if (type.name === 'Array' || Array.isArray(type)) {
        if (!(self.__data[p] instanceof List) &amp;&amp;
            self.__data[p] !== undefined &amp;&amp;
            self.__data[p] !== null) {
          self.__data[p] = List(self.__data[p], type, self);
        }
      }
    }
  }
  this.trigger('initialize');
}</_initProperties>
      <getPropertyType>function (propName) {
  return this.constructor.getPropertyType(propName);
}</getPropertyType>
      <toObject>function (onlySchema, removeHidden, removeProtected) {
  if (onlySchema === undefined) {
    onlySchema = true;
  }
  var data = {};
  var self = this;
  var Model = this.constructor;

  // if it is already an Object
  if (Model === Object) {
    return self;
  }

  var strict = this.__strict;
  var schemaLess = (strict === false) || !onlySchema;
  var persistUndefinedAsNull = Model.definition.settings.persistUndefinedAsNull;

  var props = Model.definition.properties;
  var keys = Object.keys(props);
  var propertyName, val;

  for (var i = 0; i &lt; keys.length; i++) {
    propertyName = keys[i];
    val = self[propertyName];

    // Exclude functions
    if (typeof val === 'function') {
      continue;
    }
    // Exclude hidden properties
    if (removeHidden &amp;&amp; Model.isHiddenProperty(propertyName)) {
      continue;
    }

    if (removeProtected &amp;&amp; Model.isProtectedProperty(propertyName)) {
      continue;
    }

    if (val instanceof List) {
      data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
    } else {
      if (val !== undefined &amp;&amp; val !== null &amp;&amp; val.toObject) {
        data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
      } else {
        if (val === undefined &amp;&amp; persistUndefinedAsNull) {
          val = null;
        }
        data[propertyName] = val;
      }
    }
  }

  if (schemaLess) {
    // Find its own properties which can be set via myModel.myProperty = 'myValue'.
    // If the property is not declared in the model definition, no setter will be
    // triggered to add it to __data
    keys = Object.keys(self);
    var size = keys.length;
    for (i = 0; i &lt; size; i++) {
      propertyName = keys[i];
      if (props[propertyName]) {
        continue;
      }
      if (propertyName.indexOf('__') === 0) {
        continue;
      }
      if (removeHidden &amp;&amp; Model.isHiddenProperty(propertyName)) {
        continue;
      }
      if (removeProtected &amp;&amp; Model.isProtectedProperty(propertyName)) {
        continue;
      }
      if (data[propertyName] !== undefined) {
        continue;
      }
      val = self[propertyName];
      if (val !== undefined) {
        if (typeof val === 'function') {
          continue;
        }
        if (val !== null &amp;&amp; val.toObject) {
          data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
        } else {
          data[propertyName] = val;
        }
      } else if (persistUndefinedAsNull) {
        data[propertyName] = null;
      }
    }
    // Now continue to check __data
    keys = Object.keys(self.__data);
    size = keys.length;
    for (i = 0; i &lt; size; i++) {
      propertyName = keys[i];
      if (propertyName.indexOf('__') === 0) {
        continue;
      }
      if (data[propertyName] === undefined) {
        if (removeHidden &amp;&amp; Model.isHiddenProperty(propertyName)) {
          continue;
        }
        if (removeProtected &amp;&amp; Model.isProtectedProperty(propertyName)) {
          continue;
        }
        var ownVal = self[propertyName];
        // The ownVal can be a relation function
        val = (ownVal !== undefined &amp;&amp; (typeof ownVal !== 'function')) ? ownVal : self.__data[propertyName];
        if (typeof val === 'function') {
          continue;
        }

        if (val !== undefined &amp;&amp; val !== null &amp;&amp; val.toObject) {
          data[propertyName] = val.toObject(!schemaLess, removeHidden, true);
        } else if (val === undefined &amp;&amp; persistUndefinedAsNull) {
          data[propertyName] = null;
        } else {
          data[propertyName] = val;
        }
      }
    }
  }

  return data;
}</toObject>
      <toJSON>function () {
  return this.toObject(false, true, false);
}</toJSON>
      <fromObject>function (obj) {
  for (var key in obj) {
    this[key] = obj[key];
  }
}</fromObject>
      <reset>function () {
  var obj = this;
  for (var k in obj) {
    if (k !== 'id' &amp;&amp; !obj.constructor.dataSource.definitions[obj.constructor.modelName].properties[k]) {
      delete obj[k];
    }
  }
}</reset>
      <inspect>function (depth) {
  if (INSPECT_SUPPORTS_OBJECT_RETVAL)
    return this.__data;

  // Workaround for older versions
  // See also https://github.com/joyent/node/commit/66280de133
  return util.inspect(this.__data, {
    showHidden: false,
    depth: depth,
    colors: false,
  });
}</inspect>
      <getDataSource>function () {
  return this.__dataSource || this.constructor.dataSource;
}</getDataSource>
      <setStrict>function (strict) {
  this.__strict = strict;
}</setStrict>
      <trigger>function trigger(actionName, work, data, callback) {
  var capitalizedName = capitalize(actionName);
  var beforeHook = this.constructor['before' + capitalizedName] ||
    this.constructor['pre' + capitalizedName];
  var afterHook = this.constructor['after' + capitalizedName] ||
    this.constructor['post' + capitalizedName];
  if (actionName === 'validate') {
    beforeHook = beforeHook || this.constructor.beforeValidation;
    afterHook = afterHook || this.constructor.afterValidation;
  }
  var inst = this;

  if (actionName !== 'initialize') {
    if (beforeHook)
      deprecateHook(inst.constructor, ['before', 'pre'], capitalizedName);
    if (afterHook)
      deprecateHook(inst.constructor, ['after', 'post'], capitalizedName);
  }

  // we only call "before" hook when we have actual action (work) to perform
  if (work) {
    if (beforeHook) {
      // before hook should be called on instance with two parameters: next and data
      beforeHook.call(inst, function() {
        // Check arguments to next(err, result)
        if (arguments.length) {
          return callback &amp;&amp; callback.apply(null, arguments);
        }
        // No err &amp; result is present, proceed with the real work
        // actual action also have one param: callback
        work.call(inst, next);
      }, data);
    } else {
      work.call(inst, next);
    }
  } else {
    next();
  }

  function next(done) {
    if (afterHook) {
      afterHook.call(inst, done);
    } else if (done) {
      done.call(this);
    }
  }
}</trigger>
      <isValid>function (callback, data, options) {
  options = options || {};
  var valid = true, inst = this, wait = 0, async = false;
  var validations = this.constructor.validations;

  var reportDiscardedProperties = this.__strict &amp;&amp;
    this.__unknownProperties &amp;&amp; this.__unknownProperties.length;

  // exit with success when no errors
  if (typeof validations !== 'object' &amp;&amp; !reportDiscardedProperties) {
    cleanErrors(this);
    if (callback) {
      this.trigger('validate', function(validationsDone) {
        validationsDone.call(inst, function() {
          callback(valid);
        });
      }, data, callback);
    }
    return valid;
  }

  Object.defineProperty(this, 'errors', {
    enumerable: false,
    configurable: true,
    value: new Errors,
  });

  this.trigger('validate', function(validationsDone) {
    var inst = this,
      asyncFail = false;

    var attrs = Object.keys(validations || {});

    attrs.forEach(function(attr) {
      var attrValidations = validations[attr] || [];
      attrValidations.forEach(function(v) {
        if (v.options &amp;&amp; v.options.async) {
          async = true;
          wait += 1;
          process.nextTick(function() {
            validationFailed(inst, attr, v, options, done);
          });
        } else {
          if (validationFailed(inst, attr, v)) {
            valid = false;
          }
        }
      });
    });

    if (reportDiscardedProperties) {
      for (var ix in inst.__unknownProperties) {
        var key = inst.__unknownProperties[ix];
        var code = 'unknown-property';
        var msg = defaultMessages[code];
        inst.errors.add(key, msg, code);
        valid = false;
      }
    }

    if (!async) {
      validationsDone.call(inst, function() {
        if (valid) cleanErrors(inst);
        if (callback) {
          callback(valid);
        }
      });
    }

    function done(fail) {
      asyncFail = asyncFail || fail;
      if (--wait === 0) {
        validationsDone.call(inst, function() {
          if (valid &amp;&amp; !asyncFail) cleanErrors(inst);
          if (callback) {
            callback(valid &amp;&amp; !asyncFail);
          }
        });
      }
    }
  }, data, callback);

  if (async) {
    // in case of async validation we should return undefined here,
    // because not all validations are finished yet
    return;
  } else {
    return valid;
  }
}</isValid>
    </ns1:authenticate>
  </soap:Body>
</soap:Envelope>HTTP/1.1 500 Internal Server Error
Server: Apache-Coyote/1.1
Content-Type: text/xml;charset=UTF-8
Content-Length: 323
Date: Thu, 08 Jun 2017 02:40:26 GMT
Connection: close

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><soap:Fault><faultcode>soap:Client</faultcode><faultstring>Unmarshalling Error: 意外的元素 (uri:"", local:"__cachedRelations")。所需元素为&lt;{}password>,&lt;{}user_at_domain> </faultstring></soap:Fault></soap

Link to sample repo to reproduce issue (if bug)

Expected result

Actual result (if bug)

Additional information (Node.js version, LoopBack version, etc)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions