When side-posting, doesn't find relatedObject for deeply nested validation errors
Opened this issue · 3 comments
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:
@niels Your issue might be related to the one I just posted #38 -- see my comments there.
I think the fix is two-fold:
-
if
ID
s are going to be strings in graphiti responses thengraphiti_errors
should make theid
in the validation payload a string -- or spraypaint can force compare them as strings. -
Spraypaint should check the
temp_id
/temp-id
for it's presence before potentially comparing twoundefined
values for equality.
I provided a standalone Javascript demonstration of the bug.
I submitted a PR here: #39
@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?