graphiti-api/spraypaint.js

When side-posting, doesn't find relatedObject for deeply nested validation errors

Opened this issue · 3 comments

niels commented

Forgive me if this is an operator error, but I think we're seeing Spraypaint being unable to cope with graphiti produced validation errors on (deeply) nested relationships.

We have a Delivery 1 <> n ContentsVersions 1 <> n Contents relationship which is modelled server-side roughly like so:

# Models:

class Delivery < ApplicationRecord
  has_many :contents_versions
end

class ContentsVersions < ApplicationRecord
  belongs_to :delivery
  has_many :contents

  validates :total_price, numericality: { greater_than: 0 }
end

class Contents < ApplicationRecord
  belongs_to :contents_version
end

# Resources:

class DeliveryResource < ApplicationResource
  attribute :courier_name, :string

  has_many :contents_versions
end

class ContentsVersionResource < ApplicationResource
  attribute :total_price, :big_decimal

  belongs_to :delivery
  has_many :contents
end

class ContentResource < ApplicationResource
  attribute :product_id, :string,
  attribute :qty, :integer

  belongs_to :contents_version
end

Our Spraypaint models correspondingly look something like this:

const Delivery = ApplicationRecord.extend({
  attrs: {
    contents_versions: hasMany(),
    courier_name: attr(),
  },
  static: {
    jsonapiType: 'deliveries',
  },
});

const DeliveriesContentsVersion = ApplicationRecord.extend({
  attrs: {
    contents: hasMany(),
    delivery: belongsTo(),
    delivery_id: attr(),
  },

  static: {
    jsonapiType: 'contents_versions',
  },
});

const DeliveriesContent = ApplicationRecord.extend({
  attrs: {
    contents_version: belongsTo(),
    contents_version_id: attr(),
    product_id: attr(),
    qty: attr(),
  },

  static: {
    jsonapiType: 'contents',
  },
});

When we try to create a Delivery side-posting a ContentsVersion with a Content, but the ContentsVersion fails to validate server-side, we get this error:

TypeError: "relatedObject is undefined"
    _processRelationship validation-error-builder.js:56
    apply validation-error-builder.js:20
    apply validation-error-builder.js:16
    apply validation-error-builder.js:8
    _handleResponse model.js:764

The error gets raised when trying to parse the error related to the Contents#content_version presence validation. Please find a full copy of our version of the _processRelationship function further below.

Our request payload looks like this:

{
    "data": {
        "attributes": {
            "courier_name": "DPD"
        },
        "relationships": {
            "contents_versions": {
                "data": [
                    {
                        "temp-id": "temp-id-12",
                        "method": "create",
                        "type": "contents_versions"
                    }
                ]
            }
        },
        "type": "deliveries"
    },
    "included": [
        {
            "temp-id": "temp-id-12",
            "attributes": {
                "total_price": "0.0"
            },
            "relationships": {
                "contents": {
                    "data": [
                        {
                            "temp-id": "temp-id-13",
                            "method": "create",
                            "type": "contents"
                        }
                    ]
                }
            },
            "type": "contents_versions"
        },
        {
            "temp-id": "temp-id-13",
            "attributes": {
                "product_id": "1",
                "qty": 1
            },
            "type": "contents"
        }
    ]
}

Note that this payload format seems to be correct. When we send data that passes the validation, all resources get created correctly. With invalid data, however, the servers responds as follows:

{
    "errors": [
        {
            "status": "422",
            "code": "unprocessable_entity",
            "source": {
                "pointer": "/data/attributes/total_price"
            },
            "meta": {
                "relationship": {
                    "attribute": "total_price",
                    "temp-id": "temp-id-12",
                    "name": "contents_versions",
                    "code": "zero_price",
                    "type": "contents_versions",
                    "message": "must be larger than 0"
                }
            },
            "title": "Validation Error",
            "detail": "Total price must be larger than 0"
        },
        {
            "status": "422",
            "code": "unprocessable_entity",
            "source": {
                "pointer": "/data/relationships/contents_version"
            },
            "meta": {
                "relationship": {
                    "attribute": "contents_version",
                    "temp-id": "temp-id-13",
                    "name": "contents",
                    "code": "blank",
                    "type": "contents",
                    "message": "must exist"
                }
            },
            "title": "Validation Error",
            "detail": "Contents version must exist"
        }
    ]
}

From this error response, Spraypaint does not seem able to figure out that the second object relates to the deeply nested Delivery => ContentsVersion => Content.

ValidationErrorBuilder.prototype._processRelationship = function (model, meta, err) {
        var relatedObject = model[model.klass.deserializeKey(meta.name)];
        if (Array.isArray(relatedObject)) {
            relatedObject = relatedObject.find(function (r) {
                return r.id === meta.id || r.temp_id === meta["temp-id"];
            });
        }
        if (meta.relationship) {
            this._processRelationship(relatedObject, meta.relationship, err);
        }
        else {
            var relatedAccumulator_1 = {};
            this._processResource(relatedAccumulator_1, meta, err);
            // make sure to assign a new error object, instead of mutating
            // the existing one, otherwise js frameworks with object tracking
            // won't be able to keep up. Validate vue.js when changing this code:
            var newErrs_1 = {};
            Object.keys(relatedObject.errors).forEach(function (key) {
                newErrs_1[key] = relatedObject.errors[key];
            });
            Object.keys(relatedAccumulator_1).forEach(function (key) {
                var error = relatedAccumulator_1[key];
                if (error !== undefined) {
                    newErrs_1[key] = error;
                }
            });
            relatedObject.errors = newErrs_1;
        }
    };

Annotated with payloads:

js

@niels Your issue might be related to the one I just posted #38 -- see my comments there.

I think the fix is two-fold:

  1. if IDs are going to be strings in graphiti responses then graphiti_errors should make the id in the validation payload a string -- or spraypaint can force compare them as strings.

  2. Spraypaint should check the temp_id / temp-id for it's presence before potentially comparing two undefined values for equality.

I provided a standalone Javascript demonstration of the bug.

I submitted a PR here: #39

elDub commented

@niels: I ran into issues related to deeply nested errors as well (see graphiti-api/graphiti_errors#8). graphiti_errors is not returning enough information for Spraypaint to properly target the correct object.

Considering using this library but old issues that are not dealt with or closed gives pause. Is this actively maintained?