diff --git a/src/util/validation-error-builder.ts b/src/util/validation-error-builder.ts index eac69c6..b6131f7 100644 --- a/src/util/validation-error-builder.ts +++ b/src/util/validation-error-builder.ts @@ -65,7 +65,7 @@ export class ValidationErrorBuilder { meta: JsonapiErrorMeta, err: JsonapiError ) { - let relatedObject = model[meta.name] + let relatedObject = model[model.klass.deserializeKey(meta.name)] if (Array.isArray(relatedObject)) { relatedObject = relatedObject.find(r => { return r.id === meta.id || r.temp_id === meta["temp-id"] diff --git a/test/fixtures.ts b/test/fixtures.ts index d5e5b5e..243e172 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -23,6 +23,7 @@ export class Person extends ApplicationRecord { @Attr firstName!: string | null @Attr lastName!: string | null @Attr({ type: Number }) age!: number | null + @BelongsTo({ type: "person_details" }) personDetail!: PersonDetail } @Model() @@ -39,6 +40,13 @@ export class PersonWithLinks extends Person { @Link() webView!: string } +@Model() +export class PersonDetail extends ApplicationRecord { + static jsonapiType = "person_details" + + @Attr address!: string +} + @Model({ keyCase: { server: "snake", client: "snake" } }) export class PersonWithoutCamelizedKeys extends Person { @Attr first_name!: string diff --git a/test/integration/validations.test.ts b/test/integration/validations.test.ts index 39beb57..41e7f36 100644 --- a/test/integration/validations.test.ts +++ b/test/integration/validations.test.ts @@ -1,82 +1,10 @@ import { expect, sinon, fetchMock } from "../test-helper" -import { Author, Book, Genre } from "../fixtures" +import { Author, Book, Genre, Person, PersonDetail } from "../fixtures" import { tempId } from "../../src/util/temp-id" import { SpraypaintBase, ModelRecord } from "../../src/model" import { ValidationError } from "../../src/validation-errors" -const mockErrors = { - firstName: { - code: "unprocessable_entity", - status: "422", - title: "Validation Error", - detail: "First Name cannot be blank", - meta: { attribute: "first_name", message: "cannot be blank" } - }, - lastName: { - code: "unprocessable_entity", - status: "422", - title: "Validation Error", - detail: "Last Name cannot be blank", - meta: { attribute: "last-name", message: "cannot be blank" } - }, - bookTitle: { - code: "unprocessable_entity", - status: "422", - title: "Validation Error", - detail: "Title cannot be blank", - meta: { - relationship: { - name: "books", - type: "books", - ["temp-id"]: "abc1", - attribute: "title", - message: "cannot be blank" - } - } - }, - bookGenreName: { - code: "unprocessable_entity", - status: "422", - title: "Validation Error", - detail: "Name cannot be blank", - meta: { - relationship: { - name: "books", - type: "books", - ["temp-id"]: "abc1", - relationship: { - name: "genre", - type: "genres", - id: "1", - attribute: "name", - message: "cannot be blank" - } - } - } - }, - bookGenreBase: { - code: "unprocessable_entity", - status: "422", - title: "Validation Error", - detail: "base some error", - meta: { - relationship: { - name: "books", - type: "books", - ["temp-id"]: "abc1", - relationship: { - name: "genre", - type: "genres", - id: "1", - attribute: "base", - message: "some error" - } - } - } - } -} as any - -const resetMocks = () => { +const resetMocks = (mockErrors: any) => { fetchMock.restore() let errors = [] @@ -96,11 +24,84 @@ const resetMocks = () => { }) } -let instance: Author let tempIdIndex = 0 -describe("validations", () => { +describe("validations (1/2)", () => { + const mockErrors = { + firstName: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "First Name cannot be blank", + meta: { attribute: "first_name", message: "cannot be blank" } + }, + lastName: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "Last Name cannot be blank", + meta: { attribute: "last-name", message: "cannot be blank" } + }, + bookTitle: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "Title cannot be blank", + meta: { + relationship: { + name: "books", + type: "books", + ["temp-id"]: "abc1", + attribute: "title", + message: "cannot be blank" + } + } + }, + bookGenreName: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "Name cannot be blank", + meta: { + relationship: { + name: "books", + type: "books", + ["temp-id"]: "abc1", + relationship: { + name: "genre", + type: "genres", + id: "1", + attribute: "name", + message: "cannot be blank" + } + } + } + }, + bookGenreBase: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "base some error", + meta: { + relationship: { + name: "books", + type: "books", + ["temp-id"]: "abc1", + relationship: { + name: "genre", + type: "genres", + id: "1", + attribute: "base", + message: "some error" + } + } + } + } + } as any + + let instance: Author + beforeEach(() => { - resetMocks() + resetMocks(mockErrors) }) beforeEach(() => { @@ -108,7 +109,6 @@ describe("validations", () => { tempIdIndex++ return `abc${tempIdIndex}` }) - instance = new Author({ lastName: "King" }) const genre = new Genre({ id: "1" }) genre.isPersisted = true @@ -279,3 +279,64 @@ describe("validations", () => { }) }) }) + +describe("validations (2/2)", () => { + const mockErrors = { + personDetailBase: { + code: "unprocessable_entity", + status: "422", + title: "Validation Error", + detail: "base some error", + meta: { + relationship: { + name: "person_detail", + type: "person_details", + ["temp-id"]: "abc1", + id: "1", + attribute: "base", + message: "some error" + } + } + } + } as any + + let instance: Person + + beforeEach(() => { + resetMocks(mockErrors) + }) + + beforeEach(() => { + sinon.stub(tempId, "generate").callsFake(() => { + tempIdIndex++ + return `abc${tempIdIndex}` + }) + + const personDetail = new PersonDetail({ id: "1" }) + personDetail.isPersisted = true + instance = new Person({ + personDetail + }) + }) + + afterEach(() => { + tempIdIndex = 0 + ;(tempId.generate).restore() + }) + + it("applies errors to nested belongsTo relationships with underscores", async () => { + const isSuccess = await instance.save({ with: ["person_details"] }) + expect(instance.isPersisted).to.eq(false) + expect(isSuccess).to.eq(false) + expect(instance.personDetail.errors).to.deep.equal({ + base: { + title: "Validation Error", + attribute: "base", + code: "unprocessable_entity", + fullMessage: "some error", + message: "some error", + rawPayload: mockErrors.personDetailBase + } + }) + }) +})